mirror of
https://github.com/mblanke/Lottery-Tracker.git
synced 2026-03-01 06:00:21 -05:00
Initial commit with dev backbone template
This commit is contained in:
57
.dockerignore
Normal file
57
.dockerignore
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
*.egg-info/
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
.turbo/
|
||||||
|
.vercel/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Docs
|
||||||
|
*.md
|
||||||
|
!DOCKER_README.md
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
test_*.py
|
||||||
|
*_test.py
|
||||||
|
|
||||||
|
# Excel files (not needed in container)
|
||||||
|
*.xlsx
|
||||||
|
|
||||||
|
# Email config (sensitive)
|
||||||
|
.env
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.log
|
||||||
|
.cache/
|
||||||
9
.github/copilot-instructions.md
vendored
Normal file
9
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
Follow `AGENTS.md` and `SKILLS.md`.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Use the PLAN → IMPLEMENT → VERIFY → REVIEW loop.
|
||||||
|
- Keep model selection on Auto unless AGENTS.md role routing says to override for that role.
|
||||||
|
- Never claim "done" unless DoD passes (`./scripts/dod.sh` or `\scripts\dod.ps1`).
|
||||||
|
- Keep diffs small and add/update tests when behavior changes.
|
||||||
|
- Prefer reproducible commands and cite sources for generated documents.
|
||||||
17
.github/workflows/dod.yml
vendored
Normal file
17
.github/workflows/dod.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
name: DoD Gate
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dod:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Run DoD
|
||||||
|
run: |
|
||||||
|
chmod +x ./scripts/dod.sh || true
|
||||||
|
./scripts/dod.sh
|
||||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Python
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
78
AGENTS.md
Normal file
78
AGENTS.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
|
||||||
|
# Agent Operating System (Auto-first)
|
||||||
|
|
||||||
|
Default: use **Auto** model selection. Only override the model when a role below says it is worth it.
|
||||||
|
|
||||||
|
## Prime directive
|
||||||
|
- Never claim "done" unless **DoD passes**.
|
||||||
|
- Prefer small, reviewable diffs.
|
||||||
|
- If requirements are unclear: stop and produce a PLAN + questions inside the plan.
|
||||||
|
- Agents talk; **DoD decides**.
|
||||||
|
|
||||||
|
## Always follow this loop
|
||||||
|
1) PLAN: goal, constraints, assumptions, steps (≤10), files to touch, test plan.
|
||||||
|
2) IMPLEMENT: smallest correct change.
|
||||||
|
3) VERIFY: run DoD until green.
|
||||||
|
4) REVIEW: summarize changes, risks, next steps.
|
||||||
|
|
||||||
|
## DoD Gate (Definition of Done)
|
||||||
|
Required before "done":
|
||||||
|
- macOS/Linux: `./scripts/dod.sh`
|
||||||
|
- Windows: `\scripts\dod.ps1`
|
||||||
|
|
||||||
|
If DoD cannot be run, say exactly why and what would be run.
|
||||||
|
|
||||||
|
## Terminal agent workflow (Copilot CLI)
|
||||||
|
Preferred terminal assistant: GitHub Copilot CLI via `gh copilot`.
|
||||||
|
|
||||||
|
Default loop:
|
||||||
|
1) Plan: draft plan + file list + test plan.
|
||||||
|
2) Build: implement smallest slice.
|
||||||
|
3) Steer: when stuck, ask for next action using current errors/logs.
|
||||||
|
4) Verify: run DoD until green.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Keep diffs small.
|
||||||
|
- If the same error repeats twice, switch to Reviewer role and produce a fix plan.
|
||||||
|
|
||||||
|
## Role routing (choose a role explicitly)
|
||||||
|
|
||||||
|
### Planner
|
||||||
|
Use when: new feature, refactor, multi-file, uncertain scope.
|
||||||
|
Output: plan + acceptance criteria + risks + test plan.
|
||||||
|
Model: Auto (override to a “high reasoning / Codex” model only for complex design/debugging).
|
||||||
|
|
||||||
|
### UI/UX Specialist
|
||||||
|
Use when: screens, layout, copy, design tradeoffs, component structure.
|
||||||
|
Output: component outline, UX notes, acceptance criteria.
|
||||||
|
Model: Auto (override to Gemini only when UI/UX is the main work).
|
||||||
|
|
||||||
|
### Coder
|
||||||
|
Use when: writing/editing code, plumbing, tests, small refactors.
|
||||||
|
Rules: follow repo conventions; keep diff small; add/update tests when behavior changes.
|
||||||
|
Model: Auto (override to Claude Haiku only when speed matters and the change is well-scoped).
|
||||||
|
|
||||||
|
### Reviewer
|
||||||
|
Use when: before merge, failing tests, risky changes, security-sensitive areas.
|
||||||
|
Output: concrete issues + recommended fixes + risk assessment + verification suggestions.
|
||||||
|
Model: Auto (override to the strongest available for high-stakes diffs).
|
||||||
|
|
||||||
|
## Non-negotiables
|
||||||
|
- Do not expose secrets/tokens/keys. Never print env vars.
|
||||||
|
- No destructive commands unless explicitly required and narrowly scoped.
|
||||||
|
- Do not add new dependencies without stating why + impact + alternatives.
|
||||||
|
- Prefer deterministic, reproducible steps.
|
||||||
|
- Cite sources when generating documents from a knowledge base.
|
||||||
|
|
||||||
|
## Repo facts (fill these in)
|
||||||
|
- Primary stack:
|
||||||
|
- Package manager:
|
||||||
|
- Test command:
|
||||||
|
- Lint/format command:
|
||||||
|
- Build command (if any):
|
||||||
|
- Deployment (if any):
|
||||||
|
|
||||||
|
|
||||||
|
## Claude Code Agents (optional)
|
||||||
|
- `.claude/agents/architect-cyber.md` — architecture + security + ops decisions for cyber apps.
|
||||||
|
- Add more agents in `.claude/agents/` as you standardize roles (reviewer, tester, security-lens).
|
||||||
255
DOCKER_QUICKSTART.md
Normal file
255
DOCKER_QUICKSTART.md
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
# 🐋 Docker Setup Complete!
|
||||||
|
|
||||||
|
## What's Been Created
|
||||||
|
|
||||||
|
### Docker Files
|
||||||
|
- ✅ `Dockerfile.backend` - Flask API with Playwright
|
||||||
|
- ✅ `Dockerfile.frontend` - Next.js app optimized for production
|
||||||
|
- ✅ `Dockerfile.email` - Email scheduler service
|
||||||
|
- ✅ `docker-compose.yml` - Development setup
|
||||||
|
- ✅ `docker-compose.prod.yml` - Production setup with nginx
|
||||||
|
- ✅ `.dockerignore` - Optimized build context
|
||||||
|
- ✅ `requirements.txt` - Python dependencies
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- ✅ Updated `next.config.ts` for standalone output
|
||||||
|
- ✅ Created startup scripts (Windows & Linux)
|
||||||
|
- ✅ Complete documentation in `DOCKER_README.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Option 1: Windows Script (Easiest)
|
||||||
|
```bash
|
||||||
|
docker-start.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Docker Compose
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Manual Build
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
docker-compose build
|
||||||
|
|
||||||
|
# Start
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 What Gets Deployed
|
||||||
|
|
||||||
|
### Backend Container
|
||||||
|
- Python 3.13
|
||||||
|
- Flask API on port 5000
|
||||||
|
- Playwright with Chromium browser
|
||||||
|
- Lottery scrapers for all 4 lotteries
|
||||||
|
- Investment calculator
|
||||||
|
- Health check endpoint
|
||||||
|
|
||||||
|
### Frontend Container
|
||||||
|
- Node.js 20
|
||||||
|
- Next.js standalone build
|
||||||
|
- Optimized production bundle
|
||||||
|
- Connects to backend API
|
||||||
|
|
||||||
|
### Email Container (Optional)
|
||||||
|
- Runs daily at 7:00 AM
|
||||||
|
- Sends lottery jackpot emails
|
||||||
|
- Uses same scraping logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Access Points
|
||||||
|
|
||||||
|
After running `docker-compose up -d`:
|
||||||
|
|
||||||
|
- **Frontend**: http://localhost:3000
|
||||||
|
- **Backend API**: http://localhost:5000
|
||||||
|
- **Health Check**: http://localhost:5000/api/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Container Management
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
```bash
|
||||||
|
# All services
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Specific service
|
||||||
|
docker-compose logs -f backend
|
||||||
|
docker-compose logs -f frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart Services
|
||||||
|
```bash
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop Everything
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild After Changes
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
### Port Already in Use
|
||||||
|
If ports 3000 or 5000 are busy:
|
||||||
|
|
||||||
|
**Option A**: Stop other services
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
netstat -ano | findstr :3000
|
||||||
|
taskkill /PID <PID> /F
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B**: Change ports in `docker-compose.yml`
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "8080:3000" # Use port 8080 instead
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Won't Start
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker logs lottery-backend
|
||||||
|
|
||||||
|
# Rebuild without cache
|
||||||
|
docker-compose build --no-cache backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Can't Connect
|
||||||
|
Update `docker-compose.yml` frontend environment:
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_API_URL=http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Include Email Scheduler
|
||||||
|
|
||||||
|
To run the email scheduler:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose --profile email up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Or remove the `profiles` section from `docker-compose.yml` to always include it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Production Deployment
|
||||||
|
|
||||||
|
### Use Production Compose
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy to Server
|
||||||
|
```bash
|
||||||
|
# On your server
|
||||||
|
git clone <your-repo>
|
||||||
|
cd Lottery
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Push to Docker Hub
|
||||||
|
```bash
|
||||||
|
# Login
|
||||||
|
docker login
|
||||||
|
|
||||||
|
# Tag images
|
||||||
|
docker tag lottery-backend yourusername/lottery-backend:latest
|
||||||
|
docker tag lottery-frontend yourusername/lottery-frontend:latest
|
||||||
|
|
||||||
|
# Push
|
||||||
|
docker push yourusername/lottery-backend:latest
|
||||||
|
docker push yourusername/lottery-frontend:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Security for Production
|
||||||
|
|
||||||
|
1. **Use environment variables** - Don't hardcode credentials
|
||||||
|
2. **Enable HTTPS** - Use nginx with SSL certificates
|
||||||
|
3. **Update base images** regularly
|
||||||
|
4. **Scan for vulnerabilities**:
|
||||||
|
```bash
|
||||||
|
docker scan lottery-backend
|
||||||
|
```
|
||||||
|
5. **Use Docker secrets** for sensitive data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 Data Persistence
|
||||||
|
|
||||||
|
Currently, containers are stateless. To add persistence:
|
||||||
|
|
||||||
|
Add volumes in `docker-compose.yml`:
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎛️ Resource Limits
|
||||||
|
|
||||||
|
Current limits (production):
|
||||||
|
- Backend: 2GB RAM, 1 CPU
|
||||||
|
- Frontend: 512MB RAM, 0.5 CPU
|
||||||
|
- Email: 1GB RAM, 0.5 CPU
|
||||||
|
|
||||||
|
Adjust in `docker-compose.prod.yml` if needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Benefits of Docker
|
||||||
|
|
||||||
|
1. ✅ **Consistent environment** - Works the same everywhere
|
||||||
|
2. ✅ **Easy deployment** - One command to start everything
|
||||||
|
3. ✅ **Isolation** - Each service in its own container
|
||||||
|
4. ✅ **Scalability** - Easy to scale services
|
||||||
|
5. ✅ **Version control** - Docker images are versioned
|
||||||
|
6. ✅ **Portability** - Deploy anywhere Docker runs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Next Steps
|
||||||
|
|
||||||
|
1. ✅ Test locally: `docker-compose up -d`
|
||||||
|
2. ✅ Check logs: `docker-compose logs -f`
|
||||||
|
3. ✅ Access app: http://localhost:3000
|
||||||
|
4. ✅ Configure email scheduler if needed
|
||||||
|
5. ✅ Deploy to production server
|
||||||
|
6. ✅ Set up CI/CD pipeline (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Need Help?
|
||||||
|
|
||||||
|
See detailed documentation in:
|
||||||
|
- `DOCKER_README.md` - Full Docker guide
|
||||||
|
- `EMAIL_SETUP.md` - Email configuration
|
||||||
|
- Docker logs: `docker-compose logs -f`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Enjoy your Dockerized Lottery Investment Calculator! 🎰🐋
|
||||||
347
DOCKER_README.md
Normal file
347
DOCKER_README.md
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
# Lottery Investment Calculator - Docker Setup
|
||||||
|
|
||||||
|
## 🐋 Docker Deployment Guide
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Docker Desktop installed (https://www.docker.com/products/docker-desktop)
|
||||||
|
- Docker Compose (included with Docker Desktop)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### 1. Build and Run Everything
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start:
|
||||||
|
- **Backend API** on http://localhost:5000
|
||||||
|
- **Frontend Web App** on http://localhost:3000
|
||||||
|
|
||||||
|
### 2. Check Status
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. View Logs
|
||||||
|
```bash
|
||||||
|
# All services
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Just backend
|
||||||
|
docker-compose logs -f backend
|
||||||
|
|
||||||
|
# Just frontend
|
||||||
|
docker-compose logs -f frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Stop Everything
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Individual Services
|
||||||
|
|
||||||
|
### Backend Only
|
||||||
|
```bash
|
||||||
|
docker build -f Dockerfile.backend -t lottery-backend .
|
||||||
|
docker run -p 5000:5000 lottery-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Only
|
||||||
|
```bash
|
||||||
|
docker build -f Dockerfile.frontend -t lottery-frontend .
|
||||||
|
docker run -p 3000:3000 lottery-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Email Scheduler (Optional)
|
||||||
|
```bash
|
||||||
|
docker-compose --profile email up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Update Next.js to use standalone output
|
||||||
|
|
||||||
|
Add to `frontend/next.config.ts`:
|
||||||
|
```typescript
|
||||||
|
const nextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Create `.env` file:
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
FLASK_ENV=production
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:5000
|
||||||
|
|
||||||
|
# Email (optional)
|
||||||
|
EMAIL_SENDER=mblanke@gmail.com
|
||||||
|
EMAIL_RECIPIENT=mblanke@gmail.com
|
||||||
|
EMAIL_PASSWORD=vyapvyjjfrqpqnax
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update `docker-compose.yml` to use env_file:
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
env_file: .env
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Build Process
|
||||||
|
|
||||||
|
### First Time Setup
|
||||||
|
```bash
|
||||||
|
# Build all images
|
||||||
|
docker-compose build
|
||||||
|
|
||||||
|
# Or build individually
|
||||||
|
docker-compose build backend
|
||||||
|
docker-compose build frontend
|
||||||
|
docker-compose build email-scheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild After Code Changes
|
||||||
|
```bash
|
||||||
|
# Rebuild and restart
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Rebuild specific service
|
||||||
|
docker-compose up -d --build backend
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Network Configuration
|
||||||
|
|
||||||
|
All services communicate via the `lottery-network` bridge network.
|
||||||
|
|
||||||
|
### Internal URLs (container to container):
|
||||||
|
- Backend: `http://backend:5000`
|
||||||
|
- Frontend: `http://frontend:3000`
|
||||||
|
|
||||||
|
### External URLs (host to container):
|
||||||
|
- Backend: `http://localhost:5000`
|
||||||
|
- Frontend: `http://localhost:3000`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Health Checks
|
||||||
|
|
||||||
|
The backend includes a health check endpoint:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:5000/api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Check in Docker:
|
||||||
|
```bash
|
||||||
|
docker inspect lottery-backend | grep -A 10 Health
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Production Deployment
|
||||||
|
|
||||||
|
### Docker Hub
|
||||||
|
```bash
|
||||||
|
# Tag images
|
||||||
|
docker tag lottery-backend yourusername/lottery-backend:latest
|
||||||
|
docker tag lottery-frontend yourusername/lottery-frontend:latest
|
||||||
|
|
||||||
|
# Push to Docker Hub
|
||||||
|
docker push yourusername/lottery-backend:latest
|
||||||
|
docker push yourusername/lottery-frontend:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy to Server
|
||||||
|
```bash
|
||||||
|
# Pull images on server
|
||||||
|
docker pull yourusername/lottery-backend:latest
|
||||||
|
docker pull yourusername/lottery-frontend:latest
|
||||||
|
|
||||||
|
# Run with compose
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Backend won't start
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker logs lottery-backend
|
||||||
|
|
||||||
|
# Common issues:
|
||||||
|
# - Port 5000 already in use
|
||||||
|
# - Playwright installation failed
|
||||||
|
# - Missing dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend can't connect to backend
|
||||||
|
```bash
|
||||||
|
# Check if backend is running
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Test backend directly
|
||||||
|
curl http://localhost:5000/api/health
|
||||||
|
|
||||||
|
# Check frontend environment
|
||||||
|
docker exec lottery-frontend env | grep API_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
### Playwright browser issues
|
||||||
|
```bash
|
||||||
|
# Rebuild with no cache
|
||||||
|
docker-compose build --no-cache backend
|
||||||
|
|
||||||
|
# Check Playwright installation
|
||||||
|
docker exec lottery-backend playwright --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container keeps restarting
|
||||||
|
```bash
|
||||||
|
# View logs
|
||||||
|
docker logs lottery-backend --tail 100
|
||||||
|
|
||||||
|
# Check health status
|
||||||
|
docker inspect lottery-backend | grep -A 5 Health
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Useful Commands
|
||||||
|
|
||||||
|
### Access Container Shell
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
docker exec -it lottery-backend /bin/bash
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
docker exec -it lottery-frontend /bin/sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove Everything
|
||||||
|
```bash
|
||||||
|
# Stop and remove containers, networks
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Also remove volumes
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# Remove images
|
||||||
|
docker-compose down --rmi all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prune Unused Resources
|
||||||
|
```bash
|
||||||
|
docker system prune -a
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Resource Usage
|
||||||
|
```bash
|
||||||
|
docker stats
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚢 Alternative: Docker without Compose
|
||||||
|
|
||||||
|
### Create Network
|
||||||
|
```bash
|
||||||
|
docker network create lottery-network
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Backend
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name lottery-backend \
|
||||||
|
--network lottery-network \
|
||||||
|
-p 5000:5000 \
|
||||||
|
lottery-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Frontend
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name lottery-frontend \
|
||||||
|
--network lottery-network \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-e NEXT_PUBLIC_API_URL=http://localhost:5000 \
|
||||||
|
lottery-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Email Scheduler with Docker
|
||||||
|
|
||||||
|
To include the email scheduler:
|
||||||
|
|
||||||
|
1. **Start with email service:**
|
||||||
|
```bash
|
||||||
|
docker-compose --profile email up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Or add to default profile** (edit docker-compose.yml):
|
||||||
|
Remove `profiles: - email` from email-scheduler service
|
||||||
|
|
||||||
|
3. **Check email logs:**
|
||||||
|
```bash
|
||||||
|
docker logs lottery-email -f
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Security Notes
|
||||||
|
|
||||||
|
⚠️ **Important:**
|
||||||
|
- Never commit `.env` files with real credentials
|
||||||
|
- Use Docker secrets in production
|
||||||
|
- Set proper firewall rules
|
||||||
|
- Use HTTPS in production
|
||||||
|
- Regularly update base images
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Scaling
|
||||||
|
|
||||||
|
### Run multiple backend instances
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --scale backend=3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add load balancer (nginx)
|
||||||
|
See `docker-compose.prod.yml` for nginx configuration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
If containers won't start:
|
||||||
|
1. Check Docker Desktop is running
|
||||||
|
2. Ensure ports 3000 and 5000 are available
|
||||||
|
3. Check logs: `docker-compose logs`
|
||||||
|
4. Rebuild: `docker-compose up -d --build`
|
||||||
|
5. Reset: `docker-compose down && docker-compose up -d`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Image Sizes (Approximate)
|
||||||
|
|
||||||
|
- Backend: ~1.5 GB (includes Chromium browser)
|
||||||
|
- Frontend: ~200 MB
|
||||||
|
- Email Scheduler: ~1.5 GB (includes Chromium browser)
|
||||||
|
|
||||||
|
To reduce size, consider multi-stage builds or Alpine Linux variants.
|
||||||
57
Dockerfile.backend
Normal file
57
Dockerfile.backend
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Flask Backend Dockerfile
|
||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies for Playwright
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
wget \
|
||||||
|
gnupg \
|
||||||
|
ca-certificates \
|
||||||
|
fonts-liberation \
|
||||||
|
libasound2 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libatk1.0-0 \
|
||||||
|
libatspi2.0-0 \
|
||||||
|
libcups2 \
|
||||||
|
libdbus-1-3 \
|
||||||
|
libdrm2 \
|
||||||
|
libgbm1 \
|
||||||
|
libgtk-3-0 \
|
||||||
|
libnspr4 \
|
||||||
|
libnss3 \
|
||||||
|
libwayland-client0 \
|
||||||
|
libxcomposite1 \
|
||||||
|
libxdamage1 \
|
||||||
|
libxfixes3 \
|
||||||
|
libxkbcommon0 \
|
||||||
|
libxrandr2 \
|
||||||
|
xdg-utils \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Install Playwright browsers
|
||||||
|
RUN playwright install chromium
|
||||||
|
RUN playwright install-deps chromium
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY app.py .
|
||||||
|
COPY lottery_calculator.py .
|
||||||
|
COPY ["import requests.py", "."]
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV FLASK_APP=app.py
|
||||||
|
ENV FLASK_ENV=production
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["python", "app.py"]
|
||||||
46
Dockerfile.email
Normal file
46
Dockerfile.email
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Email Scheduler Dockerfile
|
||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies for Playwright
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
wget \
|
||||||
|
gnupg \
|
||||||
|
ca-certificates \
|
||||||
|
fonts-liberation \
|
||||||
|
libasound2 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libatk1.0-0 \
|
||||||
|
libatspi2.0-0 \
|
||||||
|
libcups2 \
|
||||||
|
libdbus-1-3 \
|
||||||
|
libdrm2 \
|
||||||
|
libgbm1 \
|
||||||
|
libgtk-3-0 \
|
||||||
|
libnspr4 \
|
||||||
|
libnss3 \
|
||||||
|
libwayland-client0 \
|
||||||
|
libxcomposite1 \
|
||||||
|
libxdamage1 \
|
||||||
|
libxfixes3 \
|
||||||
|
libxkbcommon0 \
|
||||||
|
libxrandr2 \
|
||||||
|
xdg-utils \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements and install
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Install Playwright
|
||||||
|
RUN playwright install chromium
|
||||||
|
RUN playwright install-deps chromium
|
||||||
|
|
||||||
|
# Copy email sender script
|
||||||
|
COPY email_sender.py .
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
CMD ["python", "email_sender.py"]
|
||||||
47
Dockerfile.frontend
Normal file
47
Dockerfile.frontend
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Next.js Frontend Dockerfile
|
||||||
|
FROM node:20-alpine AS base
|
||||||
|
|
||||||
|
# Install dependencies only when needed
|
||||||
|
FROM base AS deps
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY frontend/ .
|
||||||
|
|
||||||
|
# Accept build argument for API URL (defaults to localhost for browser access)
|
||||||
|
ARG NEXT_PUBLIC_API_URL=http://localhost:5000
|
||||||
|
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production image, copy all the files and run next
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
208
EMAIL_SETUP.md
Normal file
208
EMAIL_SETUP.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# Email Configuration for Lottery Jackpot Alerts
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Install Required Package
|
||||||
|
```bash
|
||||||
|
pip install schedule
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Email Settings
|
||||||
|
|
||||||
|
Edit `email_sender.py` and update the `EMAIL_CONFIG` section:
|
||||||
|
|
||||||
|
```python
|
||||||
|
EMAIL_CONFIG = {
|
||||||
|
'smtp_server': 'smtp.gmail.com', # Your email provider's SMTP server
|
||||||
|
'smtp_port': 587,
|
||||||
|
'sender_email': 'your-email@gmail.com', # Your email address
|
||||||
|
'sender_password': 'your-app-password', # Your app-specific password
|
||||||
|
'recipient_email': 'recipient@example.com', # Where to send the report
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Email Provider Settings
|
||||||
|
|
||||||
|
#### For Gmail:
|
||||||
|
1. **Enable 2-Factor Authentication** on your Google account
|
||||||
|
2. **Generate App Password**:
|
||||||
|
- Go to: https://myaccount.google.com/apppasswords
|
||||||
|
- Select "Mail" and "Windows Computer"
|
||||||
|
- Copy the 16-character password
|
||||||
|
- Use this password in `sender_password` (NOT your regular Gmail password)
|
||||||
|
3. SMTP Settings:
|
||||||
|
- Server: `smtp.gmail.com`
|
||||||
|
- Port: `587`
|
||||||
|
|
||||||
|
#### For Outlook/Hotmail:
|
||||||
|
- Server: `smtp-mail.outlook.com`
|
||||||
|
- Port: `587`
|
||||||
|
- Use your regular email and password
|
||||||
|
|
||||||
|
#### For Yahoo:
|
||||||
|
- Server: `smtp.mail.yahoo.com`
|
||||||
|
- Port: `587`
|
||||||
|
- Generate app password at: https://login.yahoo.com/account/security
|
||||||
|
|
||||||
|
#### For Other Providers:
|
||||||
|
Search for "[Your Provider] SMTP settings" to find the correct server and port.
|
||||||
|
|
||||||
|
### 4. Test the Email
|
||||||
|
|
||||||
|
Uncomment this line in the `main()` function to send a test email immediately:
|
||||||
|
```python
|
||||||
|
send_daily_jackpots()
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
```bash
|
||||||
|
python email_sender.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Schedule Daily Emails
|
||||||
|
|
||||||
|
The script is configured to send emails at **7:00 AM** every day.
|
||||||
|
|
||||||
|
To run it continuously:
|
||||||
|
```bash
|
||||||
|
python email_sender.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the terminal window open. The script will:
|
||||||
|
- Wait until 7:00 AM
|
||||||
|
- Fetch current jackpots
|
||||||
|
- Send formatted email
|
||||||
|
- Repeat daily
|
||||||
|
|
||||||
|
### 6. Run as Background Service (Optional)
|
||||||
|
|
||||||
|
#### Windows - Task Scheduler:
|
||||||
|
1. Open Task Scheduler
|
||||||
|
2. Create Basic Task
|
||||||
|
3. Name: "Lottery Jackpot Email"
|
||||||
|
4. Trigger: Daily at 7:00 AM
|
||||||
|
5. Action: Start a program
|
||||||
|
- Program: `python`
|
||||||
|
- Arguments: `d:\Projects\Dev\Lottery\email_sender.py`
|
||||||
|
6. Finish
|
||||||
|
|
||||||
|
#### Windows - NSSM (Non-Sucking Service Manager):
|
||||||
|
```bash
|
||||||
|
# Install NSSM
|
||||||
|
choco install nssm
|
||||||
|
|
||||||
|
# Create service
|
||||||
|
nssm install LotteryEmail python d:\Projects\Dev\Lottery\email_sender.py
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
nssm start LotteryEmail
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linux - Cron Job:
|
||||||
|
```bash
|
||||||
|
# Edit crontab
|
||||||
|
crontab -e
|
||||||
|
|
||||||
|
# Add this line (runs at 7:00 AM daily)
|
||||||
|
0 7 * * * /usr/bin/python3 /path/to/email_sender.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linux - systemd service:
|
||||||
|
Create `/etc/systemd/system/lottery-email.service`:
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Lottery Jackpot Email Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=yourusername
|
||||||
|
WorkingDirectory=/path/to/Lottery
|
||||||
|
ExecStart=/usr/bin/python3 /path/to/email_sender.py
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable lottery-email
|
||||||
|
sudo systemctl start lottery-email
|
||||||
|
```
|
||||||
|
|
||||||
|
## Email Features
|
||||||
|
|
||||||
|
The automated email includes:
|
||||||
|
- 🎰 **Powerball** jackpot (US)
|
||||||
|
- 🎰 **Mega Millions** jackpot (US)
|
||||||
|
- 🎰 **Lotto Max** jackpot (Canada - TAX FREE!)
|
||||||
|
- 🎰 **Lotto 6/49** jackpot (Canada - TAX FREE!)
|
||||||
|
- 📅 **Timestamp** of when data was fetched
|
||||||
|
- 💡 **Reminder** about Canadian tax-free winnings
|
||||||
|
- 🎨 **Beautiful HTML formatting** with colors and styling
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Change Send Time:
|
||||||
|
Edit this line in `email_sender.py`:
|
||||||
|
```python
|
||||||
|
schedule.every().day.at("07:00").do(send_daily_jackpots)
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `"09:30"` - 9:30 AM
|
||||||
|
- `"18:00"` - 6:00 PM
|
||||||
|
- `"00:00"` - Midnight
|
||||||
|
|
||||||
|
### Send to Multiple Recipients:
|
||||||
|
Change the `send_email()` function:
|
||||||
|
```python
|
||||||
|
msg['To'] = "email1@example.com, email2@example.com, email3@example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Send Multiple Times Per Day:
|
||||||
|
Add multiple schedule lines:
|
||||||
|
```python
|
||||||
|
schedule.every().day.at("07:00").do(send_daily_jackpots)
|
||||||
|
schedule.every().day.at("19:00").do(send_daily_jackpots)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Authentication failed":
|
||||||
|
- Make sure you're using an **app password**, not your regular password (for Gmail)
|
||||||
|
- Check that 2FA is enabled on your account
|
||||||
|
- Verify SMTP server and port are correct
|
||||||
|
|
||||||
|
### "Connection refused":
|
||||||
|
- Check your firewall settings
|
||||||
|
- Verify SMTP port is correct (usually 587 or 465)
|
||||||
|
- Try port 465 with `SMTP_SSL` instead of `SMTP` with `starttls()`
|
||||||
|
|
||||||
|
### Script stops running:
|
||||||
|
- Check if your computer went to sleep
|
||||||
|
- Use Task Scheduler or systemd to auto-restart
|
||||||
|
- Check logs for error messages
|
||||||
|
|
||||||
|
### Jackpots not updating:
|
||||||
|
- Websites may have changed their HTML structure
|
||||||
|
- Check if Playwright browser is installed: `playwright install chromium`
|
||||||
|
- Test the scraper functions individually
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
⚠️ **IMPORTANT**:
|
||||||
|
- Never commit `email_sender.py` with your real credentials to Git
|
||||||
|
- Use environment variables for sensitive data in production
|
||||||
|
- Keep your app password secure
|
||||||
|
- Don't share your app password with anyone
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you encounter issues:
|
||||||
|
1. Run the test email first to verify configuration
|
||||||
|
2. Check error messages in the console
|
||||||
|
3. Verify internet connection
|
||||||
|
4. Confirm email provider settings
|
||||||
|
5. Test scraping functions individually
|
||||||
23
SKILLS.md
Normal file
23
SKILLS.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Skills Index
|
||||||
|
|
||||||
|
These skill files define repeatable behaviors for agents and humans.
|
||||||
|
|
||||||
|
Agents must follow them in this order:
|
||||||
|
1) SKILLS/00-operating-model.md
|
||||||
|
2) SKILLS/05-agent-taxonomy.md
|
||||||
|
3) SKILLS/10-definition-of-done.md
|
||||||
|
4) SKILLS/20-repo-map.md (use whenever unfamiliar with the repo)
|
||||||
|
5) SKILLS/25-algorithms-performance.md
|
||||||
|
6) SKILLS/26-vibe-coding-fundamentals.md
|
||||||
|
7) SKILLS/27-performance-profiling.md
|
||||||
|
8) SKILLS/30-implementation-rules.md
|
||||||
|
9) SKILLS/40-testing-quality.md
|
||||||
|
10) SKILLS/50-pr-review.md
|
||||||
|
11) SKILLS/56-ui-material-ui.md (for React/Next portal-style apps)
|
||||||
|
12) SKILLS/60-security-safety.md
|
||||||
|
13) SKILLS/70-docs-artifacts.md
|
||||||
|
14) SKILLS/82-mcp-server-design.md (when building MCP servers/tools)
|
||||||
|
15) SKILLS/83-fastmcp-3-patterns.md (if using FastMCP 3)
|
||||||
|
16) SKILLS/80-mcp-tools.md (if this repo has MCP tools)
|
||||||
|
|
||||||
|
Rule: If anything conflicts, **AGENTS.md wins**.
|
||||||
21
SKILLS/00-operating-model.md
Normal file
21
SKILLS/00-operating-model.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
# Operating Model
|
||||||
|
|
||||||
|
## Default cadence
|
||||||
|
- Prefer iterative progress over big bangs.
|
||||||
|
- Keep diffs small: target ≤ 300 changed lines per PR unless justified.
|
||||||
|
- Update tests/docs as part of the same change when possible.
|
||||||
|
|
||||||
|
## Working agreement
|
||||||
|
- Start with a PLAN for non-trivial tasks.
|
||||||
|
- Implement the smallest slice that satisfies acceptance criteria.
|
||||||
|
- Verify via DoD.
|
||||||
|
- Write a crisp PR summary: what changed, why, and how verified.
|
||||||
|
|
||||||
|
## Stop conditions (plan first)
|
||||||
|
Stop and produce a PLAN (do not code yet) if:
|
||||||
|
- scope is unclear
|
||||||
|
- more than 3 files will change
|
||||||
|
- data model changes
|
||||||
|
- auth/security boundaries
|
||||||
|
- performance-critical paths
|
||||||
36
SKILLS/05-agent-taxonomy.md
Normal file
36
SKILLS/05-agent-taxonomy.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Agent Types & Roles (Practical Taxonomy)
|
||||||
|
|
||||||
|
Use this skill to choose the *right* kind of agent workflow for the job.
|
||||||
|
|
||||||
|
## Common agent “types” (in practice)
|
||||||
|
|
||||||
|
### 1) Chat assistant (no tools)
|
||||||
|
Best for: explanations, brainstorming, small edits.
|
||||||
|
Risk: can hallucinate; no grounding in repo state.
|
||||||
|
|
||||||
|
### 2) Tool-using single agent
|
||||||
|
Best for: well-scoped tasks where the agent can read/write files and run commands.
|
||||||
|
Key control: strict DoD gates + minimal permissions.
|
||||||
|
|
||||||
|
### 3) Planner + Executor (2-role pattern)
|
||||||
|
Best for: medium complexity work (multi-file changes, feature work).
|
||||||
|
Flow: Planner writes plan + acceptance criteria → Executor implements → Reviewer checks.
|
||||||
|
|
||||||
|
### 4) Multi-agent (specialists)
|
||||||
|
Best for: bigger features with separable workstreams (UI, backend, docs, tests).
|
||||||
|
Rule: isolate context per role; use separate branches/worktrees.
|
||||||
|
|
||||||
|
### 5) Supervisor / orchestrator
|
||||||
|
Best for: long-running workflows with checkpoints (pipelines, report generation, PAD docs).
|
||||||
|
Rule: supervisor delegates, enforces gates, and composes final output.
|
||||||
|
|
||||||
|
## Decision rules (fast)
|
||||||
|
- If you can describe it in ≤ 5 steps → single tool-using agent.
|
||||||
|
- If you need tradeoffs/design → Planner + Executor.
|
||||||
|
- If UI + backend + docs/tests all move → multi-agent specialists.
|
||||||
|
- If it’s a pipeline that runs repeatedly → orchestrator.
|
||||||
|
|
||||||
|
## Guardrails (always)
|
||||||
|
- DoD is the truth gate.
|
||||||
|
- Separate branches/worktrees for parallel work.
|
||||||
|
- Log decisions + commands in AGENT_LOG.md.
|
||||||
24
SKILLS/10-definition-of-done.md
Normal file
24
SKILLS/10-definition-of-done.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
# Definition of Done (DoD)
|
||||||
|
|
||||||
|
A change is "done" only when:
|
||||||
|
|
||||||
|
## Code correctness
|
||||||
|
- Builds successfully (if applicable)
|
||||||
|
- Tests pass
|
||||||
|
- Linting/formatting passes
|
||||||
|
- Types/checks pass (if applicable)
|
||||||
|
|
||||||
|
## Quality
|
||||||
|
- No new warnings introduced
|
||||||
|
- Edge cases handled (inputs validated, errors meaningful)
|
||||||
|
- Hot paths not regressed (if applicable)
|
||||||
|
|
||||||
|
## Hygiene
|
||||||
|
- No secrets committed
|
||||||
|
- Docs updated if behavior or usage changed
|
||||||
|
- PR summary includes verification steps
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
- macOS/Linux: `./scripts/dod.sh`
|
||||||
|
- Windows: `\scripts\dod.ps1`
|
||||||
16
SKILLS/20-repo-map.md
Normal file
16
SKILLS/20-repo-map.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
# Repo Mapping Skill
|
||||||
|
|
||||||
|
When entering a repo:
|
||||||
|
1) Read README.md
|
||||||
|
2) Identify entrypoints (app main / server startup / CLI)
|
||||||
|
3) Identify config (env vars, .env.example, config files)
|
||||||
|
4) Identify test/lint scripts (package.json, pyproject.toml, Makefile, etc.)
|
||||||
|
5) Write a 10-line "repo map" in the PLAN before changing code
|
||||||
|
|
||||||
|
Output format:
|
||||||
|
- Purpose:
|
||||||
|
- Key modules:
|
||||||
|
- Data flow:
|
||||||
|
- Commands:
|
||||||
|
- Risks:
|
||||||
20
SKILLS/25-algorithms-performance.md
Normal file
20
SKILLS/25-algorithms-performance.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Algorithms & Performance
|
||||||
|
|
||||||
|
Use this skill when performance matters (large inputs, hot paths, or repeated calls).
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- Identify the **state** you’re recomputing.
|
||||||
|
- Add **memoization / caching** when the same subproblem repeats.
|
||||||
|
- Prefer **linear scans** + caches over nested loops when possible.
|
||||||
|
- If you can write it as a **recurrence**, you can test it.
|
||||||
|
|
||||||
|
## Practical heuristics
|
||||||
|
- Measure first when possible (timing + input sizes).
|
||||||
|
- Optimize the biggest wins: avoid repeated I/O, repeated parsing, repeated network calls.
|
||||||
|
- Keep caches bounded (size/TTL) and invalidate safely.
|
||||||
|
- Choose data structures intentionally: dict/set for membership, heap for top-k, deque for queues.
|
||||||
|
|
||||||
|
## Review notes (for PRs)
|
||||||
|
- Call out accidental O(n²) patterns.
|
||||||
|
- Suggest table/DP or memoization when repeated work is obvious.
|
||||||
|
- Add tests that cover base cases + typical cases + worst-case size.
|
||||||
31
SKILLS/26-vibe-coding-fundamentals.md
Normal file
31
SKILLS/26-vibe-coding-fundamentals.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Vibe Coding With Fundamentals (Safety Rails)
|
||||||
|
|
||||||
|
Use this skill when you’re using “vibe coding” (fast, conversational building) but want production-grade outcomes.
|
||||||
|
|
||||||
|
## The good
|
||||||
|
- Rapid scaffolding and iteration
|
||||||
|
- Fast UI prototypes
|
||||||
|
- Quick exploration of architectures and options
|
||||||
|
|
||||||
|
## The failure mode
|
||||||
|
- “It works on my machine” code with weak tests
|
||||||
|
- Security foot-guns (auth, input validation, secrets)
|
||||||
|
- Performance cliffs (accidental O(n²), repeated I/O)
|
||||||
|
- Unmaintainable abstractions
|
||||||
|
|
||||||
|
## Safety rails (apply every time)
|
||||||
|
- Always start with acceptance criteria (what “done” means).
|
||||||
|
- Prefer small PRs; never dump a huge AI diff.
|
||||||
|
- Require DoD gates (lint/test/build) before merge.
|
||||||
|
- Write tests for behavior changes.
|
||||||
|
- For anything security/data related: do a Reviewer pass.
|
||||||
|
|
||||||
|
## When to slow down
|
||||||
|
- Auth/session/token work
|
||||||
|
- Anything touching payments, PII, secrets
|
||||||
|
- Data migrations/schema changes
|
||||||
|
- Performance-critical paths
|
||||||
|
- “It’s flaky” or “it only fails in CI”
|
||||||
|
|
||||||
|
## Practical prompt pattern (use in PLAN)
|
||||||
|
- “State assumptions, list files to touch, propose tests, and include rollback steps.”
|
||||||
31
SKILLS/27-performance-profiling.md
Normal file
31
SKILLS/27-performance-profiling.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Performance Profiling (Bun/Node)
|
||||||
|
|
||||||
|
Use this skill when:
|
||||||
|
- a hot path feels slow
|
||||||
|
- CPU usage is high
|
||||||
|
- you suspect accidental O(n²) or repeated work
|
||||||
|
- you need evidence before optimizing
|
||||||
|
|
||||||
|
## Bun CPU profiling
|
||||||
|
Bun supports CPU profiling via `--cpu-prof` (generates a `.cpuprofile` you can open in Chrome DevTools).
|
||||||
|
|
||||||
|
Upcoming: `bun --cpu-prof-md <script>` outputs a CPU profile as **Markdown** so LLMs can read/grep it easily.
|
||||||
|
|
||||||
|
### Workflow (Bun)
|
||||||
|
1) Run the workload with profiling enabled
|
||||||
|
- Today: `bun --cpu-prof ./path/to/script.ts`
|
||||||
|
- Upcoming: `bun --cpu-prof-md ./path/to/script.ts`
|
||||||
|
2) Save the output (or `.cpuprofile`) into `./profiles/` with a timestamp.
|
||||||
|
3) Ask the Reviewer agent to:
|
||||||
|
- identify the top 5 hottest functions
|
||||||
|
- propose the smallest fix
|
||||||
|
- add a regression test or benchmark
|
||||||
|
|
||||||
|
## Node CPU profiling (fallback)
|
||||||
|
- `node --cpu-prof ./script.js` writes a `.cpuprofile` file.
|
||||||
|
- Open in Chrome DevTools → Performance → Load profile.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
- Optimize based on measured hotspots, not vibes.
|
||||||
|
- Prefer algorithmic wins (remove repeated work) over micro-optimizations.
|
||||||
|
- Keep profiling artifacts out of git unless explicitly needed (use `.gitignore`).
|
||||||
16
SKILLS/30-implementation-rules.md
Normal file
16
SKILLS/30-implementation-rules.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
# Implementation Rules
|
||||||
|
|
||||||
|
## Change policy
|
||||||
|
- Prefer edits over rewrites.
|
||||||
|
- Keep changes localized.
|
||||||
|
- One change = one purpose.
|
||||||
|
- Avoid unnecessary abstraction.
|
||||||
|
|
||||||
|
## Dependency policy
|
||||||
|
- Default: do not add dependencies.
|
||||||
|
- If adding: explain why, alternatives considered, and impact.
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
- Validate inputs at boundaries.
|
||||||
|
- Error messages must be actionable: what failed + what to do next.
|
||||||
14
SKILLS/40-testing-quality.md
Normal file
14
SKILLS/40-testing-quality.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
# Testing & Quality
|
||||||
|
|
||||||
|
## Strategy
|
||||||
|
- If behavior changes: add/update tests.
|
||||||
|
- Unit tests for logic; integration tests for boundaries; E2E only where needed.
|
||||||
|
|
||||||
|
## Minimum for every PR
|
||||||
|
- A test plan in the PR summary (even if “existing tests cover this”).
|
||||||
|
- Run DoD.
|
||||||
|
|
||||||
|
## Flaky tests
|
||||||
|
- Capture repro steps.
|
||||||
|
- Quarantine only with justification + follow-up issue.
|
||||||
16
SKILLS/50-pr-review.md
Normal file
16
SKILLS/50-pr-review.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
# PR Review Skill
|
||||||
|
|
||||||
|
Reviewer must check:
|
||||||
|
- Correctness: does it do what it claims?
|
||||||
|
- Safety: secrets, injection, auth boundaries
|
||||||
|
- Maintainability: readability, naming, duplication
|
||||||
|
- Tests: added/updated appropriately
|
||||||
|
- DoD: did it pass?
|
||||||
|
|
||||||
|
Reviewer output format:
|
||||||
|
1) Summary
|
||||||
|
2) Must-fix
|
||||||
|
3) Nice-to-have
|
||||||
|
4) Risks
|
||||||
|
5) Verification suggestions
|
||||||
41
SKILLS/56-ui-material-ui.md
Normal file
41
SKILLS/56-ui-material-ui.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Material UI (MUI) Design System
|
||||||
|
|
||||||
|
Use this skill for any React/Next “portal/admin/dashboard” UI so you stay consistent and avoid random component soup.
|
||||||
|
|
||||||
|
## Standard choice
|
||||||
|
- Preferred UI library: **MUI (Material UI)**.
|
||||||
|
- Prefer MUI components over ad-hoc HTML/CSS unless there’s a good reason.
|
||||||
|
- One design system per repo (do not mix Chakra/Ant/Bootstrap/etc.).
|
||||||
|
|
||||||
|
## Setup (Next.js/React)
|
||||||
|
- Install: `@mui/material @emotion/react @emotion/styled`
|
||||||
|
- If using icons: `@mui/icons-material`
|
||||||
|
- If using data grid: `@mui/x-data-grid` (or pro if licensed)
|
||||||
|
|
||||||
|
## Theming rules
|
||||||
|
- Define a single theme (typography, spacing, palette) and reuse everywhere.
|
||||||
|
- Use semantic colors (primary/secondary/error/warning/success/info), not hard-coded hex everywhere.
|
||||||
|
- Prefer MUI’s `sx` for small styling; use `styled()` for reusable components.
|
||||||
|
|
||||||
|
## “Portal” patterns (modals, popovers, menus)
|
||||||
|
- Use MUI Dialog/Modal/Popover/Menu components instead of DIY portals.
|
||||||
|
- Accessibility requirements:
|
||||||
|
- Focus is trapped in Dialog/Modal.
|
||||||
|
- Escape closes modal unless explicitly prevented.
|
||||||
|
- All inputs have labels; buttons have clear text/aria-labels.
|
||||||
|
- Keyboard navigation works end-to-end.
|
||||||
|
|
||||||
|
## Layout conventions (for portals)
|
||||||
|
- Use: AppBar + Drawer (or NavigationRail equivalent) + main content.
|
||||||
|
- Keep pages as composition of small components: Page → Sections → Widgets.
|
||||||
|
- Keep forms consistent: FormControl + helper text + validation messages.
|
||||||
|
|
||||||
|
## Performance hygiene
|
||||||
|
- Avoid re-render storms: memoize heavy lists; use virtualization for large tables (DataGrid).
|
||||||
|
- Prefer server pagination for huge datasets.
|
||||||
|
|
||||||
|
## PR review checklist
|
||||||
|
- Theme is used (no random styling).
|
||||||
|
- Components are MUI where reasonable.
|
||||||
|
- Modal/popover accessibility is correct.
|
||||||
|
- No mixed UI libraries.
|
||||||
15
SKILLS/60-security-safety.md
Normal file
15
SKILLS/60-security-safety.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
# Security & Safety
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
- Never output secrets or tokens.
|
||||||
|
- Never log sensitive inputs.
|
||||||
|
- Never commit credentials.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
- Validate external inputs at boundaries.
|
||||||
|
- Fail closed for auth/security decisions.
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
- No destructive commands unless requested and scoped.
|
||||||
|
- Prefer read-only operations first.
|
||||||
13
SKILLS/70-docs-artifacts.md
Normal file
13
SKILLS/70-docs-artifacts.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
# Docs & Artifacts
|
||||||
|
|
||||||
|
Update documentation when:
|
||||||
|
- setup steps change
|
||||||
|
- env vars change
|
||||||
|
- endpoints/CLI behavior changes
|
||||||
|
- data formats change
|
||||||
|
|
||||||
|
Docs standards:
|
||||||
|
- Provide copy/paste commands
|
||||||
|
- Provide expected outputs where helpful
|
||||||
|
- Keep it short and accurate
|
||||||
11
SKILLS/80-mcp-tools.md
Normal file
11
SKILLS/80-mcp-tools.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
# MCP Tools Skill (Optional)
|
||||||
|
|
||||||
|
If this repo defines MCP servers/tools:
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Tool calls must be explicit and logged.
|
||||||
|
- Maintain an allowlist of tools; deny by default.
|
||||||
|
- Every tool must have: purpose, inputs/outputs schema, examples, and tests.
|
||||||
|
- Prefer idempotent tool operations.
|
||||||
|
- Never add tools that can exfiltrate secrets without strict guards.
|
||||||
51
SKILLS/82-mcp-server-design.md
Normal file
51
SKILLS/82-mcp-server-design.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# MCP Server Design (Agent-First)
|
||||||
|
|
||||||
|
Build MCP servers like you’re designing a UI for a non-human user.
|
||||||
|
|
||||||
|
This skill distills Phil Schmid’s MCP server best practices into concrete repo rules.
|
||||||
|
Source: “MCP is Not the Problem, It’s your Server” (Jan 21, 2026).
|
||||||
|
|
||||||
|
## 1) Outcomes, not operations
|
||||||
|
- Do **not** wrap REST endpoints 1:1 as tools.
|
||||||
|
- Expose high-level, outcome-oriented tools.
|
||||||
|
- Bad: `get_user`, `list_orders`, `get_order_status`
|
||||||
|
- Good: `track_latest_order(email)` (server orchestrates internally)
|
||||||
|
|
||||||
|
## 2) Flatten arguments
|
||||||
|
- Prefer top-level primitives + constrained enums.
|
||||||
|
- Avoid nested `dict`/config objects (agents hallucinate keys).
|
||||||
|
- Defaults reduce decision load.
|
||||||
|
|
||||||
|
## 3) Instructions are context
|
||||||
|
- Tool docstrings are *instructions*:
|
||||||
|
- when to use the tool
|
||||||
|
- argument formatting rules
|
||||||
|
- what the return means
|
||||||
|
- Error strings are also context:
|
||||||
|
- return actionable, self-correcting messages (not raw stack traces)
|
||||||
|
|
||||||
|
## 4) Curate ruthlessly
|
||||||
|
- Aim for **5–15 tools** per server.
|
||||||
|
- One server, one job. Split by persona if needed.
|
||||||
|
- Delete unused tools. Don’t dump raw data into context.
|
||||||
|
|
||||||
|
## 5) Name tools for discovery
|
||||||
|
- Avoid generic names (`create_issue`).
|
||||||
|
- Prefer `{service}_{action}_{resource}`:
|
||||||
|
- `velociraptor_run_hunt`
|
||||||
|
- `github_list_prs`
|
||||||
|
- `slack_send_message`
|
||||||
|
|
||||||
|
## 6) Paginate large results
|
||||||
|
- Always support `limit` (default ~20–50).
|
||||||
|
- Return metadata: `has_more`, `next_offset`, `total_count`.
|
||||||
|
- Never return hundreds of rows unbounded.
|
||||||
|
|
||||||
|
## Repo conventions
|
||||||
|
- Put MCP tool specs in `mcp/` (schemas, examples, fixtures).
|
||||||
|
- Provide at least 1 “golden path” example call per tool.
|
||||||
|
- Add an eval that checks:
|
||||||
|
- tool names follow discovery convention
|
||||||
|
- args are flat + typed
|
||||||
|
- responses are concise + stable
|
||||||
|
- pagination works
|
||||||
40
SKILLS/83-fastmcp-3-patterns.md
Normal file
40
SKILLS/83-fastmcp-3-patterns.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# FastMCP 3 Patterns (Providers + Transforms)
|
||||||
|
|
||||||
|
Use this skill when you are building MCP servers in Python and want:
|
||||||
|
- composable tool sets
|
||||||
|
- per-user/per-session behavior
|
||||||
|
- auth, versioning, observability, and long-running tasks
|
||||||
|
|
||||||
|
## Mental model (FastMCP 3)
|
||||||
|
FastMCP 3 treats everything as three composable primitives:
|
||||||
|
- **Components**: what you expose (tools, resources, prompts)
|
||||||
|
- **Providers**: where components come from (decorators, files, OpenAPI, remote MCP, etc.)
|
||||||
|
- **Transforms**: how you reshape what clients see (namespace, filters, auth, versioning, visibility)
|
||||||
|
|
||||||
|
## Recommended architecture for Marc’s platform
|
||||||
|
Build a **single “Cyber MCP Gateway”** that composes providers:
|
||||||
|
- LocalProvider: core cyber tools (run hunt, parse triage, generate report)
|
||||||
|
- OpenAPIProvider: wrap stable internal APIs (ticketing, asset DB) without 1:1 endpoint exposure
|
||||||
|
- ProxyProvider/FastMCPProvider: mount sub-servers (e.g., Velociraptor tools, Intel feeds)
|
||||||
|
|
||||||
|
Then apply transforms:
|
||||||
|
- Namespace per domain: `hunt.*`, `intel.*`, `pad.*`
|
||||||
|
- Visibility per session: hide dangerous tools unless user/role allows
|
||||||
|
- VersionFilter: keep old clients working while you evolve tools
|
||||||
|
|
||||||
|
## Production must-haves
|
||||||
|
- **Tool timeouts**: never let a tool hang forever
|
||||||
|
- **Pagination**: all list tools must be bounded
|
||||||
|
- **Background tasks**: use for long hunts / ingest jobs
|
||||||
|
- **Tracing**: emit OpenTelemetry traces so you can debug agent/tool behavior
|
||||||
|
|
||||||
|
## Auth rules
|
||||||
|
- Prefer component-level auth for “dangerous” tools.
|
||||||
|
- Default stance: read-only tools visible; write/execute tools gated.
|
||||||
|
|
||||||
|
## Versioning rules
|
||||||
|
- Version your components when you change schemas or semantics.
|
||||||
|
- Keep 1 previous version callable during migrations.
|
||||||
|
|
||||||
|
## Upgrade guidance
|
||||||
|
FastMCP 3 is in beta; pin to v2 for stability in production until you’ve tested.
|
||||||
50
analyze_excel.py
Normal file
50
analyze_excel.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import openpyxl
|
||||||
|
|
||||||
|
wb = openpyxl.load_workbook('Max.xlsx', data_only=True)
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
print("LOTTERY INVESTMENT CALCULATOR ANALYSIS")
|
||||||
|
print("="*70)
|
||||||
|
print("\nINPUTS:")
|
||||||
|
print("-"*70)
|
||||||
|
print(f"Lottery Amount: ${ws['D3'].value:,.0f}")
|
||||||
|
print(f"Cash Sum (52%): ${ws['D4'].value:,.2f}")
|
||||||
|
print(f"Federal Taxes (37%): ${ws['D5'].value:,.2f}")
|
||||||
|
print(f"State Taxes (5.5%): ${ws['D6'].value:,.2f}")
|
||||||
|
print(f"Net Amount: ${ws['D7'].value:,.2f}")
|
||||||
|
print(f"Canadian Conversion (1.35x): ${ws['D8'].value:,.2f}")
|
||||||
|
print(f"\nInvest 90%: ${ws['D10'].value:,.2f}")
|
||||||
|
print(f"Fun Money (10%): ${ws['G7'].value:,.2f}")
|
||||||
|
print(f"Net Daily Income: ${ws['G8'].value:,.2f}")
|
||||||
|
|
||||||
|
print("\n\nINVESTMENT CYCLES (90-day periods at 4.5% annual return):")
|
||||||
|
print("-"*70)
|
||||||
|
print(f"{'Cycle':<10} {'Principal Start':<18} {'Interest':<15} {'Taxes':<15} {'Withdrawal':<15} {'Total Out':<15} {'Reinvest':<15} {'Principal End':<18}")
|
||||||
|
print("-"*70)
|
||||||
|
|
||||||
|
for row in range(13, 21): # Cycles 1-8
|
||||||
|
cycle = ws[f'C{row}'].value
|
||||||
|
principal_start = ws[f'D{row}'].value
|
||||||
|
interest = ws[f'E{row}'].value
|
||||||
|
taxes = ws[f'F{row}'].value
|
||||||
|
withdrawal = ws[f'G{row}'].value
|
||||||
|
total_out = ws[f'H{row}'].value
|
||||||
|
reinvest = ws[f'I{row}'].value
|
||||||
|
principal_end = ws[f'J{row}'].value
|
||||||
|
|
||||||
|
print(f"{cycle:<10} ${principal_start:>15,.0f} ${interest:>13,.0f} ${taxes:>13,.0f} ${withdrawal:>13,.0f} ${total_out:>13,.0f} ${reinvest:>13,.0f} ${principal_end:>15,.0f}")
|
||||||
|
|
||||||
|
print("\n\nKEY FORMULAS:")
|
||||||
|
print("-"*70)
|
||||||
|
print("• Interest per cycle: Principal × 4.5% × (90/365)")
|
||||||
|
print("• Taxes on interest: Interest × 53.53%")
|
||||||
|
print("• Personal withdrawal: Interest × 10%")
|
||||||
|
print("• Total withdrawal: Taxes + Personal withdrawal")
|
||||||
|
print("• Reinvestment: Interest - Total withdrawal")
|
||||||
|
print("• Next cycle principal: Previous principal + Reinvestment")
|
||||||
|
|
||||||
|
total_withdrawn = ws['G7'].value
|
||||||
|
print(f"\n\nTOTAL PERSONAL WITHDRAWALS (8 cycles): ${total_withdrawn:,.2f}")
|
||||||
|
print(f"Average per cycle: ${total_withdrawn/8:,.2f}")
|
||||||
|
print(f"Daily income: ${ws['G8'].value:,.2f}")
|
||||||
|
print(f"Annual income: ${ws['G8'].value * 365:,.2f}")
|
||||||
172
app.py
Normal file
172
app.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
"""
|
||||||
|
Flask Backend for Lottery Investment Calculator
|
||||||
|
Provides API endpoints for jackpots and investment calculations
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, jsonify, request
|
||||||
|
from flask_cors import CORS
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import urllib3
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
import re
|
||||||
|
from lottery_calculator import calculate_us_lottery, calculate_canadian_lottery
|
||||||
|
|
||||||
|
# Suppress SSL warnings
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app) # Enable CORS for Next.js frontend
|
||||||
|
|
||||||
|
# Common headers to mimic a browser request
|
||||||
|
HEADERS = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
"Sec-Fetch-Dest": "document",
|
||||||
|
"Sec-Fetch-Mode": "navigate",
|
||||||
|
"Sec-Fetch-Site": "none",
|
||||||
|
"Cache-Control": "max-age=0",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_us_lotteries():
|
||||||
|
"""Fetch Powerball and Mega Millions jackpots"""
|
||||||
|
results = {"Powerball": None, "Mega Millions": None}
|
||||||
|
|
||||||
|
# Powerball
|
||||||
|
try:
|
||||||
|
resp = requests.get("https://www.lotto.net/powerball", timeout=10, verify=False, headers=HEADERS)
|
||||||
|
resp.raise_for_status()
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
all_text = soup.get_text()
|
||||||
|
lines = all_text.split('\n')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'Next Jackpot' in line and i + 1 < len(lines):
|
||||||
|
next_line = lines[i + 1].strip()
|
||||||
|
if '$' in next_line and 'Million' in next_line:
|
||||||
|
# Extract numeric value
|
||||||
|
match = re.search(r'\$(\d+(?:,\d+)?(?:\.\d+)?)', next_line)
|
||||||
|
if match:
|
||||||
|
value = float(match.group(1).replace(',', ''))
|
||||||
|
results["Powerball"] = value * 1_000_000
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching Powerball: {e}")
|
||||||
|
|
||||||
|
# Mega Millions
|
||||||
|
try:
|
||||||
|
resp = requests.get("https://www.lotto.net/mega-millions", timeout=10, verify=False, headers=HEADERS)
|
||||||
|
resp.raise_for_status()
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
all_text = soup.get_text()
|
||||||
|
lines = all_text.split('\n')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'Next Jackpot' in line and i + 1 < len(lines):
|
||||||
|
next_line = lines[i + 1].strip()
|
||||||
|
if '$' in next_line and 'Million' in next_line:
|
||||||
|
# Extract numeric value
|
||||||
|
match = re.search(r'\$(\d+(?:,\d+)?(?:\.\d+)?)', next_line)
|
||||||
|
if match:
|
||||||
|
value = float(match.group(1).replace(',', ''))
|
||||||
|
results["Mega Millions"] = value * 1_000_000
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching Mega Millions: {e}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def get_canadian_lotteries():
|
||||||
|
"""Fetch Lotto Max and Lotto 6/49 jackpots using Playwright"""
|
||||||
|
results = {"Lotto Max": None, "Lotto 6/49": None}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=True)
|
||||||
|
page = browser.new_page()
|
||||||
|
page.goto("https://www.olg.ca/", wait_until="networkidle", timeout=30000)
|
||||||
|
page.wait_for_timeout(3000)
|
||||||
|
content = page.content()
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
# Lotto Max
|
||||||
|
lotto_max_match = re.search(r'LOTTO\s*MAX(?:(?!LOTTO\s*6/49).)*?\$\s*([\d.,]+)\s*Million', content, re.IGNORECASE | re.DOTALL)
|
||||||
|
if lotto_max_match:
|
||||||
|
value = float(lotto_max_match.group(1).replace(',', ''))
|
||||||
|
results["Lotto Max"] = value * 1_000_000
|
||||||
|
|
||||||
|
# Lotto 6/49
|
||||||
|
lotto_649_match = re.search(r'LOTTO\s*6/49(?:(?!LOTTO\s*MAX).)*?\$\s*([\d.,]+)\s*Million', content, re.IGNORECASE | re.DOTALL)
|
||||||
|
if lotto_649_match:
|
||||||
|
value = float(lotto_649_match.group(1).replace(',', ''))
|
||||||
|
results["Lotto 6/49"] = value * 1_000_000
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching Canadian lotteries: {e}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/jackpots', methods=['GET'])
|
||||||
|
def get_jackpots():
|
||||||
|
"""API endpoint to get all lottery jackpots"""
|
||||||
|
us_lotteries = get_us_lotteries()
|
||||||
|
canadian_lotteries = get_canadian_lotteries()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"us": {
|
||||||
|
"powerball": us_lotteries["Powerball"],
|
||||||
|
"megaMillions": us_lotteries["Mega Millions"]
|
||||||
|
},
|
||||||
|
"canadian": {
|
||||||
|
"lottoMax": canadian_lotteries["Lotto Max"],
|
||||||
|
"lotto649": canadian_lotteries["Lotto 6/49"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/calculate', methods=['POST'])
|
||||||
|
def calculate():
|
||||||
|
"""API endpoint to calculate investment returns"""
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
jackpot = data.get('jackpot')
|
||||||
|
lottery_type = data.get('type', 'us') # 'us' or 'canadian'
|
||||||
|
invest_percentage = data.get('investPercentage', 0.90)
|
||||||
|
annual_return = data.get('annualReturn', 0.045)
|
||||||
|
cycles = data.get('cycles', 8)
|
||||||
|
|
||||||
|
if not jackpot:
|
||||||
|
return jsonify({"error": "Jackpot amount is required"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
if lottery_type == 'us':
|
||||||
|
result = calculate_us_lottery(jackpot, invest_percentage, annual_return, cycles)
|
||||||
|
else:
|
||||||
|
result = calculate_canadian_lottery(jackpot, invest_percentage, annual_return, cycles)
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/health', methods=['GET'])
|
||||||
|
def health():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("🎰 Lottery Investment Calculator API")
|
||||||
|
print("=" * 50)
|
||||||
|
print("Starting Flask server on http://localhost:5000")
|
||||||
|
print("API Endpoints:")
|
||||||
|
print(" - GET /api/jackpots - Get current jackpots")
|
||||||
|
print(" - POST /api/calculate - Calculate investments")
|
||||||
|
print(" - GET /api/health - Health check")
|
||||||
|
print("=" * 50)
|
||||||
|
# Bind to 0.0.0.0 so the Flask app is reachable from outside the container
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||||
94
docker-compose.prod.yml
Normal file
94
docker-compose.prod.yml
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Nginx Reverse Proxy
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: lottery-nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./ssl:/etc/nginx/ssl:ro
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
- frontend
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- lottery-network
|
||||||
|
|
||||||
|
# Flask Backend
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.backend
|
||||||
|
container_name: lottery-backend
|
||||||
|
expose:
|
||||||
|
- "5000"
|
||||||
|
environment:
|
||||||
|
- FLASK_ENV=production
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- lottery-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 1G
|
||||||
|
|
||||||
|
# Next.js Frontend
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.frontend
|
||||||
|
container_name: lottery-frontend
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_API_URL=http://backend:5000
|
||||||
|
- NODE_ENV=production
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- lottery-network
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.25'
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
# Email Scheduler
|
||||||
|
email-scheduler:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.email
|
||||||
|
container_name: lottery-email
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- lottery-network
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 1G
|
||||||
|
|
||||||
|
networks:
|
||||||
|
lottery-network:
|
||||||
|
driver: bridge
|
||||||
59
docker-compose.yml
Normal file
59
docker-compose.yml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Flask Backend
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.backend
|
||||||
|
container_name: lottery-backend
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
environment:
|
||||||
|
- FLASK_ENV=production
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- lottery-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Next.js Frontend
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.frontend
|
||||||
|
args:
|
||||||
|
NEXT_PUBLIC_API_URL: http://localhost:5000
|
||||||
|
container_name: lottery-frontend
|
||||||
|
ports:
|
||||||
|
- "3003:3000"
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_API_URL=http://localhost:5000
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- lottery-network
|
||||||
|
|
||||||
|
# Email Scheduler (Optional - runs daily at 7 AM)
|
||||||
|
email-scheduler:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.email
|
||||||
|
container_name: lottery-email
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- lottery-network
|
||||||
|
profiles:
|
||||||
|
- email # Only start if explicitly requested
|
||||||
|
|
||||||
|
networks:
|
||||||
|
lottery-network:
|
||||||
|
driver: bridge
|
||||||
75
docker-start.bat
Normal file
75
docker-start.bat
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
@echo off
|
||||||
|
echo ====================================================
|
||||||
|
echo 🎰 Lottery Investment Calculator - Docker Setup
|
||||||
|
echo ====================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Check if Docker is installed
|
||||||
|
docker --version >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ❌ Docker is not installed. Please install Docker Desktop first.
|
||||||
|
echo Download: https://www.docker.com/products/docker-desktop
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check if Docker is running
|
||||||
|
docker info >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ❌ Docker is not running. Please start Docker Desktop.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ✅ Docker is installed and running
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Build images
|
||||||
|
echo 🏗️ Building Docker images...
|
||||||
|
echo This may take 5-10 minutes on first run (downloading browsers, etc.)
|
||||||
|
echo.
|
||||||
|
docker-compose build
|
||||||
|
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ❌ Build failed. Check the errors above.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ✅ Build completed successfully!
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Start containers
|
||||||
|
echo 🚀 Starting containers...
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ❌ Failed to start containers.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ✅ All containers started successfully!
|
||||||
|
echo.
|
||||||
|
echo ====================================================
|
||||||
|
echo 🌐 Your application is now running:
|
||||||
|
echo ====================================================
|
||||||
|
echo.
|
||||||
|
echo Frontend: http://localhost:3003
|
||||||
|
echo Backend: http://localhost:5000
|
||||||
|
echo Health: http://localhost:5000/api/health
|
||||||
|
echo.
|
||||||
|
echo ====================================================
|
||||||
|
echo 📊 Useful commands:
|
||||||
|
echo ====================================================
|
||||||
|
echo.
|
||||||
|
echo View logs: docker-compose logs -f
|
||||||
|
echo Stop: docker-compose down
|
||||||
|
echo Restart: docker-compose restart
|
||||||
|
echo Rebuild: docker-compose up -d --build
|
||||||
|
echo.
|
||||||
|
echo 📖 See DOCKER_README.md for full documentation
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
68
docker-start.sh
Normal file
68
docker-start.sh
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🎰 Lottery Investment Calculator - Docker Setup"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if Docker is installed
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo "❌ Docker is not installed. Please install Docker Desktop first."
|
||||||
|
echo " Download: https://www.docker.com/products/docker-desktop"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if Docker is running
|
||||||
|
if ! docker info &> /dev/null; then
|
||||||
|
echo "❌ Docker is not running. Please start Docker Desktop."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Docker is installed and running"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build images
|
||||||
|
echo "🏗️ Building Docker images..."
|
||||||
|
echo " This may take 5-10 minutes on first run (downloading browsers, etc.)"
|
||||||
|
echo ""
|
||||||
|
docker-compose build
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "❌ Build failed. Check the errors above."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Build completed successfully!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Start containers
|
||||||
|
echo "🚀 Starting containers..."
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "❌ Failed to start containers."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ All containers started successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "================================================"
|
||||||
|
echo "🌐 Your application is now running:"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
echo " Frontend: http://localhost:3003"
|
||||||
|
echo " Backend: http://localhost:5000"
|
||||||
|
echo " Health: http://localhost:5000/api/health"
|
||||||
|
echo ""
|
||||||
|
echo "================================================"
|
||||||
|
echo "📊 Useful commands:"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
echo " View logs: docker-compose logs -f"
|
||||||
|
echo " Stop: docker-compose down"
|
||||||
|
echo " Restart: docker-compose restart"
|
||||||
|
echo " Rebuild: docker-compose up -d --build"
|
||||||
|
echo ""
|
||||||
|
echo "📖 See DOCKER_README.md for full documentation"
|
||||||
|
echo ""
|
||||||
329
email_sender.py
Normal file
329
email_sender.py
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import schedule
|
||||||
|
import time
|
||||||
|
import smtplib
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from datetime import datetime
|
||||||
|
import asyncio
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import urllib3
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Disable SSL warnings
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
# Email configuration
|
||||||
|
EMAIL_CONFIG = {
|
||||||
|
'smtp_server': 'smtp.gmail.com', # Change this for your email provider
|
||||||
|
'smtp_port': 587,
|
||||||
|
'sender_email': 'mblanke@gmail.com', # Replace with your email
|
||||||
|
'sender_password': 'vyapvyjjfrqpqnax', # App password (spaces removed)
|
||||||
|
'recipient_email': 'mblanke@gmail.com', # Replace with recipient email
|
||||||
|
}
|
||||||
|
|
||||||
|
# Common headers to mimic a browser request
|
||||||
|
HEADERS = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Scraping functions
|
||||||
|
def get_powerball():
|
||||||
|
"""Get Powerball jackpot from lotto.net"""
|
||||||
|
try:
|
||||||
|
url = "https://www.lotto.net/powerball"
|
||||||
|
response = requests.get(url, timeout=10, verify=False, headers=HEADERS)
|
||||||
|
response.raise_for_status()
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# Look for "Next Jackpot" text
|
||||||
|
all_text = soup.get_text()
|
||||||
|
lines = all_text.split('\n')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'Next Jackpot' in line and i + 1 < len(lines):
|
||||||
|
next_line = lines[i + 1].strip()
|
||||||
|
if '$' in next_line and 'Million' in next_line:
|
||||||
|
# Parse the amount
|
||||||
|
match = re.search(r'\$\s*([\d,]+(?:\.\d+)?)\s*Million', next_line)
|
||||||
|
if match:
|
||||||
|
amount_str = match.group(1).replace(',', '')
|
||||||
|
return float(amount_str)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting Powerball: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_mega_millions():
|
||||||
|
"""Get Mega Millions jackpot from lotto.net"""
|
||||||
|
try:
|
||||||
|
url = "https://www.lotto.net/mega-millions"
|
||||||
|
response = requests.get(url, timeout=10, verify=False, headers=HEADERS)
|
||||||
|
response.raise_for_status()
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# Look for "Next Jackpot" text
|
||||||
|
all_text = soup.get_text()
|
||||||
|
lines = all_text.split('\n')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'Next Jackpot' in line and i + 1 < len(lines):
|
||||||
|
next_line = lines[i + 1].strip()
|
||||||
|
if '$' in next_line and 'Million' in next_line:
|
||||||
|
# Parse the amount
|
||||||
|
match = re.search(r'\$\s*([\d,]+(?:\.\d+)?)\s*Million', next_line)
|
||||||
|
if match:
|
||||||
|
amount_str = match.group(1).replace(',', '')
|
||||||
|
return float(amount_str)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting Mega Millions: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_canadian_lotteries():
|
||||||
|
"""Get Lotto Max and Lotto 6/49 jackpots using Playwright"""
|
||||||
|
lotto_max = None
|
||||||
|
lotto_649 = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(headless=True)
|
||||||
|
page = await browser.new_page()
|
||||||
|
|
||||||
|
await page.goto('https://www.olg.ca/', wait_until='networkidle')
|
||||||
|
await page.wait_for_timeout(2000)
|
||||||
|
|
||||||
|
content = await page.content()
|
||||||
|
|
||||||
|
# Lotto Max pattern
|
||||||
|
lotto_max_pattern = r'LOTTO\s*MAX(?:(?!LOTTO\s*6/49).)*?\$\s*([\d.,]+)\s*Million'
|
||||||
|
match = re.search(lotto_max_pattern, content, re.IGNORECASE | re.DOTALL)
|
||||||
|
if match:
|
||||||
|
amount_str = match.group(1).replace(',', '')
|
||||||
|
lotto_max = float(amount_str)
|
||||||
|
|
||||||
|
# Lotto 6/49 pattern
|
||||||
|
lotto_649_pattern = r'LOTTO\s*6/49.*?\$\s*([\d.,]+)\s*Million'
|
||||||
|
match = re.search(lotto_649_pattern, content, re.IGNORECASE | re.DOTALL)
|
||||||
|
if match:
|
||||||
|
amount_str = match.group(1).replace(',', '')
|
||||||
|
lotto_649 = float(amount_str)
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting Canadian lotteries: {e}")
|
||||||
|
|
||||||
|
return lotto_max, lotto_649
|
||||||
|
|
||||||
|
def format_currency(amount):
|
||||||
|
"""Format amount as currency"""
|
||||||
|
if amount is None:
|
||||||
|
return "Not available"
|
||||||
|
return f"${amount:,.0f}M"
|
||||||
|
|
||||||
|
def create_email_html(powerball, mega_millions, lotto_max, lotto_649):
|
||||||
|
"""Create HTML email content"""
|
||||||
|
html = f"""
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {{
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: 20px;
|
||||||
|
}}
|
||||||
|
.container {{
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 30px;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}}
|
||||||
|
h1 {{
|
||||||
|
color: #2c3e50;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}}
|
||||||
|
.lottery-section {{
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}}
|
||||||
|
.lottery-section h2 {{
|
||||||
|
color: #34495e;
|
||||||
|
border-bottom: 2px solid #3498db;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}}
|
||||||
|
.lottery-item {{
|
||||||
|
background-color: #ecf0f1;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}}
|
||||||
|
.lottery-name {{
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 16px;
|
||||||
|
}}
|
||||||
|
.lottery-amount {{
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #27ae60;
|
||||||
|
}}
|
||||||
|
.tax-free {{
|
||||||
|
background-color: #2ecc71;
|
||||||
|
color: white;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}}
|
||||||
|
.footer {{
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #ecf0f1;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 12px;
|
||||||
|
}}
|
||||||
|
.timestamp {{
|
||||||
|
color: #95a5a6;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🎰 Daily Lottery Jackpots</h1>
|
||||||
|
|
||||||
|
<div class="lottery-section">
|
||||||
|
<h2>🇺🇸 US Lotteries</h2>
|
||||||
|
<div class="lottery-item">
|
||||||
|
<div>
|
||||||
|
<span class="lottery-name">Powerball</span>
|
||||||
|
</div>
|
||||||
|
<span class="lottery-amount">{format_currency(powerball)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="lottery-item">
|
||||||
|
<div>
|
||||||
|
<span class="lottery-name">Mega Millions</span>
|
||||||
|
</div>
|
||||||
|
<span class="lottery-amount">{format_currency(mega_millions)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lottery-section">
|
||||||
|
<h2>🇨🇦 Canadian Lotteries</h2>
|
||||||
|
<div class="lottery-item">
|
||||||
|
<div>
|
||||||
|
<span class="lottery-name">Lotto Max</span>
|
||||||
|
<span class="tax-free">TAX FREE</span>
|
||||||
|
</div>
|
||||||
|
<span class="lottery-amount">{format_currency(lotto_max)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="lottery-item">
|
||||||
|
<div>
|
||||||
|
<span class="lottery-name">Lotto 6/49</span>
|
||||||
|
<span class="tax-free">TAX FREE</span>
|
||||||
|
</div>
|
||||||
|
<span class="lottery-amount">{format_currency(lotto_649)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>💡 Remember: Canadian lottery winnings are tax-free!</p>
|
||||||
|
<p>📊 Visit your Lottery Investment Calculator for detailed analysis</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timestamp">
|
||||||
|
Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
return html
|
||||||
|
|
||||||
|
def send_email(subject, html_content):
|
||||||
|
"""Send email with jackpot information"""
|
||||||
|
try:
|
||||||
|
# Create message
|
||||||
|
msg = MIMEMultipart('alternative')
|
||||||
|
msg['Subject'] = subject
|
||||||
|
msg['From'] = EMAIL_CONFIG['sender_email']
|
||||||
|
msg['To'] = EMAIL_CONFIG['recipient_email']
|
||||||
|
|
||||||
|
# Attach HTML content
|
||||||
|
html_part = MIMEText(html_content, 'html')
|
||||||
|
msg.attach(html_part)
|
||||||
|
|
||||||
|
# Send email
|
||||||
|
with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
|
||||||
|
server.starttls()
|
||||||
|
server.login(EMAIL_CONFIG['sender_email'], EMAIL_CONFIG['sender_password'])
|
||||||
|
server.send_message(msg)
|
||||||
|
|
||||||
|
print(f"✅ Email sent successfully at {datetime.now().strftime('%I:%M %p')}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error sending email: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_daily_jackpots():
|
||||||
|
"""Fetch jackpots and send email"""
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"🎰 Fetching lottery jackpots at {datetime.now().strftime('%I:%M %p')}")
|
||||||
|
print(f"{'='*50}")
|
||||||
|
|
||||||
|
# Get US lotteries
|
||||||
|
print("📊 Fetching Powerball...")
|
||||||
|
powerball = get_powerball()
|
||||||
|
print(f" Powerball: {format_currency(powerball)}")
|
||||||
|
|
||||||
|
print("📊 Fetching Mega Millions...")
|
||||||
|
mega_millions = get_mega_millions()
|
||||||
|
print(f" Mega Millions: {format_currency(mega_millions)}")
|
||||||
|
|
||||||
|
# Get Canadian lotteries
|
||||||
|
print("📊 Fetching Canadian lotteries...")
|
||||||
|
lotto_max, lotto_649 = asyncio.run(get_canadian_lotteries())
|
||||||
|
print(f" Lotto Max: {format_currency(lotto_max)}")
|
||||||
|
print(f" Lotto 6/49: {format_currency(lotto_649)}")
|
||||||
|
|
||||||
|
# Create email content
|
||||||
|
subject = f"🎰 Daily Lottery Report - {datetime.now().strftime('%B %d, %Y')}"
|
||||||
|
html_content = create_email_html(powerball, mega_millions, lotto_max, lotto_649)
|
||||||
|
|
||||||
|
# Send email
|
||||||
|
print("\n📧 Sending email...")
|
||||||
|
send_email(subject, html_content)
|
||||||
|
print(f"{'='*50}\n")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function to schedule and run the email sender"""
|
||||||
|
print("🚀 Lottery Jackpot Email Scheduler Started")
|
||||||
|
print("=" * 50)
|
||||||
|
print(f"📧 Emails will be sent to: {EMAIL_CONFIG['recipient_email']}")
|
||||||
|
print(f"⏰ Scheduled time: 7:00 AM daily")
|
||||||
|
print(f"🔄 Current time: {datetime.now().strftime('%I:%M %p')}")
|
||||||
|
print("=" * 50)
|
||||||
|
print("\nPress Ctrl+C to stop the scheduler\n")
|
||||||
|
|
||||||
|
# Schedule the job for 7:00 AM every day
|
||||||
|
schedule.every().day.at("07:00").do(send_daily_jackpots)
|
||||||
|
|
||||||
|
# Optional: Uncomment to send immediately for testing
|
||||||
|
# print("🧪 Sending test email now...")
|
||||||
|
# send_daily_jackpots()
|
||||||
|
|
||||||
|
# Keep the script running
|
||||||
|
while True:
|
||||||
|
schedule.run_pending()
|
||||||
|
time.sleep(60) # Check every minute
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
frontend
Submodule
1
frontend
Submodule
Submodule frontend added at dcb2161ea4
186
import requests.py
Normal file
186
import requests.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import urllib3
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Suppress SSL warnings
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
# Common headers to mimic a browser request
|
||||||
|
HEADERS = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
"Sec-Fetch-Dest": "document",
|
||||||
|
"Sec-Fetch-Mode": "navigate",
|
||||||
|
"Sec-Fetch-Site": "none",
|
||||||
|
"Cache-Control": "max-age=0",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_powerball():
|
||||||
|
url = "https://www.lotto.net/powerball"
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, timeout=10, verify=False, headers=HEADERS)
|
||||||
|
resp.raise_for_status()
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
# Look for divs containing "Next Jackpot" and "$XXX Million"
|
||||||
|
all_text = soup.get_text()
|
||||||
|
lines = all_text.split('\n')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'Next Jackpot' in line and i + 1 < len(lines):
|
||||||
|
next_line = lines[i + 1].strip()
|
||||||
|
if '$' in next_line and 'Million' in next_line:
|
||||||
|
return next_line
|
||||||
|
return "Not found"
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error: {e}"
|
||||||
|
|
||||||
|
def get_mega_millions():
|
||||||
|
url = "https://www.lotto.net/mega-millions"
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, timeout=10, verify=False, headers=HEADERS)
|
||||||
|
resp.raise_for_status()
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
# Look for divs containing "Next Jackpot" and "$XXX Million"
|
||||||
|
all_text = soup.get_text()
|
||||||
|
lines = all_text.split('\n')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'Next Jackpot' in line and i + 1 < len(lines):
|
||||||
|
next_line = lines[i + 1].strip()
|
||||||
|
if '$' in next_line and 'Million' in next_line:
|
||||||
|
return next_line
|
||||||
|
return "Not found"
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error: {e}"
|
||||||
|
|
||||||
|
def get_lotto_max():
|
||||||
|
url = "https://www.olg.ca/"
|
||||||
|
try:
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=True)
|
||||||
|
page = browser.new_page()
|
||||||
|
page.goto(url, wait_until="networkidle", timeout=30000)
|
||||||
|
# Wait for lottery content to load
|
||||||
|
page.wait_for_timeout(3000)
|
||||||
|
content = page.content()
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
# Search for Lotto Max jackpot - look for the pattern more carefully
|
||||||
|
# Match "LOTTO MAX" followed by jackpot info, avoiding 649
|
||||||
|
match = re.search(r'LOTTO\s*MAX(?:(?!LOTTO\s*6/49).)*?\$\s*([\d.,]+)\s*Million', content, re.IGNORECASE | re.DOTALL)
|
||||||
|
if match:
|
||||||
|
return f"${match.group(1)} Million"
|
||||||
|
return "Not found"
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error: {e}"
|
||||||
|
|
||||||
|
def get_lotto_649():
|
||||||
|
url = "https://www.olg.ca/"
|
||||||
|
try:
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=True)
|
||||||
|
page = browser.new_page()
|
||||||
|
page.goto(url, wait_until="networkidle", timeout=30000)
|
||||||
|
# Wait for lottery content to load
|
||||||
|
page.wait_for_timeout(3000)
|
||||||
|
content = page.content()
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
# Search for Lotto 6/49 jackpot - be more specific
|
||||||
|
match = re.search(r'LOTTO\s*6/49(?:(?!LOTTO\s*MAX).)*?\$\s*([\d.,]+)\s*Million', content, re.IGNORECASE | re.DOTALL)
|
||||||
|
if match:
|
||||||
|
return f"${match.group(1)} Million"
|
||||||
|
return "Not found"
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error: {e}"
|
||||||
|
|
||||||
|
def get_olg_lotteries():
|
||||||
|
"""
|
||||||
|
Fetches jackpot amounts for Lotto Max and Lotto 6/49 from OLG website using Playwright.
|
||||||
|
Returns a dict with keys 'Lotto Max' and 'Lotto 6/49'.
|
||||||
|
"""
|
||||||
|
url = "https://www.olg.ca/"
|
||||||
|
results = {"Lotto Max": "Not found", "Lotto 6/49": "Not found"}
|
||||||
|
try:
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=True)
|
||||||
|
page = browser.new_page()
|
||||||
|
page.goto(url, wait_until="networkidle", timeout=30000)
|
||||||
|
# Wait for lottery content to load
|
||||||
|
page.wait_for_timeout(3000)
|
||||||
|
content = page.content()
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
# Lotto Max - be more specific to avoid 649
|
||||||
|
lotto_max_match = re.search(r'LOTTO\s*MAX(?:(?!LOTTO\s*6/49).)*?\$\s*([\d.,]+)\s*Million', content, re.IGNORECASE | re.DOTALL)
|
||||||
|
if lotto_max_match:
|
||||||
|
results["Lotto Max"] = f"${lotto_max_match.group(1)} Million"
|
||||||
|
|
||||||
|
# Lotto 6/49 - be more specific to avoid MAX
|
||||||
|
lotto_649_match = re.search(r'LOTTO\s*6/49(?:(?!LOTTO\s*MAX).)*?\$\s*([\d.,]+)\s*Million', content, re.IGNORECASE | re.DOTALL)
|
||||||
|
if lotto_649_match:
|
||||||
|
results["Lotto 6/49"] = f"${lotto_649_match.group(1)} Million"
|
||||||
|
except Exception as e:
|
||||||
|
results = {"Lotto Max": f"Error: {e}", "Lotto 6/49": f"Error: {e}"}
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_lottery_usa():
|
||||||
|
"""
|
||||||
|
Fetches jackpot amounts for Powerball and Mega Millions from lotto.net.
|
||||||
|
Returns a dict with keys 'Powerball' and 'Mega Millions'.
|
||||||
|
"""
|
||||||
|
results = {"Powerball": "Not found", "Mega Millions": "Not found"}
|
||||||
|
|
||||||
|
# Get Powerball
|
||||||
|
try:
|
||||||
|
resp = requests.get("https://www.lotto.net/powerball", timeout=10, verify=False, headers=HEADERS)
|
||||||
|
resp.raise_for_status()
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
all_text = soup.get_text()
|
||||||
|
lines = all_text.split('\n')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'Next Jackpot' in line and i + 1 < len(lines):
|
||||||
|
next_line = lines[i + 1].strip()
|
||||||
|
if '$' in next_line and 'Million' in next_line:
|
||||||
|
results["Powerball"] = next_line
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
results["Powerball"] = f"Error: {e}"
|
||||||
|
|
||||||
|
# Get Mega Millions
|
||||||
|
try:
|
||||||
|
resp = requests.get("https://www.lotto.net/mega-millions", timeout=10, verify=False, headers=HEADERS)
|
||||||
|
resp.raise_for_status()
|
||||||
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
|
all_text = soup.get_text()
|
||||||
|
lines = all_text.split('\n')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'Next Jackpot' in line and i + 1 < len(lines):
|
||||||
|
next_line = lines[i + 1].strip()
|
||||||
|
if '$' in next_line and 'Million' in next_line:
|
||||||
|
results["Mega Millions"] = next_line
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
results["Mega Millions"] = f"Error: {e}"
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("🎰 Current Lottery Jackpots")
|
||||||
|
print("------------------------------")
|
||||||
|
print(f"Powerball: {get_powerball()}")
|
||||||
|
print(f"Mega Millions: {get_mega_millions()}")
|
||||||
|
print(f"Lotto Max: {get_lotto_max()}")
|
||||||
|
print(f"Lotto 6/49: {get_lotto_649()}")
|
||||||
|
# Add OLG results as fallback/alternative
|
||||||
|
olg = get_olg_lotteries()
|
||||||
|
print(f"OLG Lotto Max: {olg['Lotto Max']}")
|
||||||
|
print(f"OLG Lotto 6/49: {olg['Lotto 6/49']}")
|
||||||
|
# Add Lottery USA results
|
||||||
|
lottery_usa = get_lottery_usa()
|
||||||
|
print(f"Lottery USA Powerball: {lottery_usa['Powerball']}")
|
||||||
|
print(f"Lottery USA Mega Millions: {lottery_usa['Mega Millions']}")
|
||||||
195
lottery_calculator.py
Normal file
195
lottery_calculator.py
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
"""
|
||||||
|
Lottery Investment Calculator
|
||||||
|
Handles both US and Canadian lottery calculations
|
||||||
|
"""
|
||||||
|
|
||||||
|
def calculate_us_lottery(jackpot, invest_percentage=0.90, annual_return=0.045, cycles=8):
|
||||||
|
"""
|
||||||
|
Calculate investment returns for US lottery winnings
|
||||||
|
|
||||||
|
Args:
|
||||||
|
jackpot: Original jackpot amount (USD)
|
||||||
|
invest_percentage: Percentage to invest (default 90%)
|
||||||
|
annual_return: Annual return rate (default 4.5%)
|
||||||
|
cycles: Number of 90-day cycles to calculate (default 8)
|
||||||
|
"""
|
||||||
|
# US Lottery calculations
|
||||||
|
cash_sum = jackpot * 0.52 # Lump sum is 52%
|
||||||
|
federal_tax = cash_sum * 0.37
|
||||||
|
state_tax = cash_sum * 0.055
|
||||||
|
net_amount = cash_sum - federal_tax - state_tax
|
||||||
|
|
||||||
|
# Convert to Canadian dollars
|
||||||
|
canadian_amount = net_amount * 1.35
|
||||||
|
|
||||||
|
# Split into investment and fun money
|
||||||
|
investment_principal = canadian_amount * invest_percentage
|
||||||
|
fun_money = canadian_amount * (1 - invest_percentage)
|
||||||
|
|
||||||
|
# Calculate cycles
|
||||||
|
cycle_results = []
|
||||||
|
principal = investment_principal
|
||||||
|
total_personal_withdrawals = 0
|
||||||
|
|
||||||
|
for cycle in range(1, cycles + 1):
|
||||||
|
# Interest for 90 days
|
||||||
|
interest_earned = principal * annual_return * (90/365)
|
||||||
|
|
||||||
|
# Taxes on investment income (53.53%)
|
||||||
|
taxes_owed = interest_earned * 0.5353
|
||||||
|
|
||||||
|
# Personal withdrawal (10% of interest)
|
||||||
|
personal_withdrawal = interest_earned * 0.10
|
||||||
|
|
||||||
|
# Total withdrawal
|
||||||
|
total_withdrawal = taxes_owed + personal_withdrawal
|
||||||
|
|
||||||
|
# Reinvestment
|
||||||
|
reinvestment = interest_earned - total_withdrawal
|
||||||
|
|
||||||
|
# New principal
|
||||||
|
new_principal = principal + reinvestment
|
||||||
|
|
||||||
|
total_personal_withdrawals += personal_withdrawal
|
||||||
|
|
||||||
|
cycle_results.append({
|
||||||
|
'cycle': cycle,
|
||||||
|
'principal_start': principal,
|
||||||
|
'interest_earned': interest_earned,
|
||||||
|
'taxes_owed': taxes_owed,
|
||||||
|
'personal_withdrawal': personal_withdrawal,
|
||||||
|
'total_withdrawal': total_withdrawal,
|
||||||
|
'reinvestment': reinvestment,
|
||||||
|
'principal_end': new_principal
|
||||||
|
})
|
||||||
|
|
||||||
|
principal = new_principal
|
||||||
|
|
||||||
|
# Calculate daily income
|
||||||
|
net_daily_income = (investment_principal * annual_return * 0.5353) / 365
|
||||||
|
|
||||||
|
return {
|
||||||
|
'country': 'US',
|
||||||
|
'original_jackpot': jackpot,
|
||||||
|
'cash_sum': cash_sum,
|
||||||
|
'federal_tax': federal_tax,
|
||||||
|
'state_tax': state_tax,
|
||||||
|
'net_amount_usd': net_amount,
|
||||||
|
'net_amount_cad': canadian_amount,
|
||||||
|
'investment_principal': investment_principal,
|
||||||
|
'fun_money': fun_money,
|
||||||
|
'net_daily_income': net_daily_income,
|
||||||
|
'annual_income': net_daily_income * 365,
|
||||||
|
'total_personal_withdrawals': total_personal_withdrawals,
|
||||||
|
'final_principal': principal,
|
||||||
|
'cycles': cycle_results
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_canadian_lottery(jackpot, invest_percentage=0.90, annual_return=0.045, cycles=8):
|
||||||
|
"""
|
||||||
|
Calculate investment returns for Canadian lottery winnings
|
||||||
|
|
||||||
|
Args:
|
||||||
|
jackpot: Original jackpot amount (CAD) - TAX FREE!
|
||||||
|
invest_percentage: Percentage to invest (default 90%)
|
||||||
|
annual_return: Annual return rate (default 4.5%)
|
||||||
|
cycles: Number of 90-day cycles to calculate (default 8)
|
||||||
|
"""
|
||||||
|
# Canadian lotteries - NO TAX on winnings!
|
||||||
|
net_amount = jackpot
|
||||||
|
|
||||||
|
# Split into investment and fun money
|
||||||
|
investment_principal = net_amount * invest_percentage
|
||||||
|
fun_money = net_amount * (1 - invest_percentage)
|
||||||
|
|
||||||
|
# Calculate cycles
|
||||||
|
cycle_results = []
|
||||||
|
principal = investment_principal
|
||||||
|
total_personal_withdrawals = 0
|
||||||
|
|
||||||
|
for cycle in range(1, cycles + 1):
|
||||||
|
# Interest for 90 days
|
||||||
|
interest_earned = principal * annual_return * (90/365)
|
||||||
|
|
||||||
|
# Taxes on investment income (53.53%)
|
||||||
|
taxes_owed = interest_earned * 0.5353
|
||||||
|
|
||||||
|
# Personal withdrawal (10% of interest)
|
||||||
|
personal_withdrawal = interest_earned * 0.10
|
||||||
|
|
||||||
|
# Total withdrawal
|
||||||
|
total_withdrawal = taxes_owed + personal_withdrawal
|
||||||
|
|
||||||
|
# Reinvestment
|
||||||
|
reinvestment = interest_earned - total_withdrawal
|
||||||
|
|
||||||
|
# New principal
|
||||||
|
new_principal = principal + reinvestment
|
||||||
|
|
||||||
|
total_personal_withdrawals += personal_withdrawal
|
||||||
|
|
||||||
|
cycle_results.append({
|
||||||
|
'cycle': cycle,
|
||||||
|
'principal_start': principal,
|
||||||
|
'interest_earned': interest_earned,
|
||||||
|
'taxes_owed': taxes_owed,
|
||||||
|
'personal_withdrawal': personal_withdrawal,
|
||||||
|
'total_withdrawal': total_withdrawal,
|
||||||
|
'reinvestment': reinvestment,
|
||||||
|
'principal_end': new_principal
|
||||||
|
})
|
||||||
|
|
||||||
|
principal = new_principal
|
||||||
|
|
||||||
|
# Calculate daily income
|
||||||
|
net_daily_income = (investment_principal * annual_return * 0.5353) / 365
|
||||||
|
|
||||||
|
return {
|
||||||
|
'country': 'Canada',
|
||||||
|
'original_jackpot': jackpot,
|
||||||
|
'net_amount_cad': net_amount,
|
||||||
|
'investment_principal': investment_principal,
|
||||||
|
'fun_money': fun_money,
|
||||||
|
'net_daily_income': net_daily_income,
|
||||||
|
'annual_income': net_daily_income * 365,
|
||||||
|
'total_personal_withdrawals': total_personal_withdrawals,
|
||||||
|
'final_principal': principal,
|
||||||
|
'cycles': cycle_results
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Test with current jackpots
|
||||||
|
print("=" * 80)
|
||||||
|
print("US LOTTERY - MEGA MILLIONS ($547M)")
|
||||||
|
print("=" * 80)
|
||||||
|
us_result = calculate_us_lottery(547_000_000)
|
||||||
|
print(f"Original Jackpot: ${us_result['original_jackpot']:,.0f}")
|
||||||
|
print(f"Cash Sum (52%): ${us_result['cash_sum']:,.0f}")
|
||||||
|
print(f"After Taxes (USD): ${us_result['net_amount_usd']:,.0f}")
|
||||||
|
print(f"After Taxes (CAD): ${us_result['net_amount_cad']:,.0f}")
|
||||||
|
print(f"Investment (90%): ${us_result['investment_principal']:,.0f}")
|
||||||
|
print(f"Fun Money (10%): ${us_result['fun_money']:,.0f}")
|
||||||
|
print(f"Daily Income: ${us_result['net_daily_income']:,.2f}")
|
||||||
|
print(f"Annual Income: ${us_result['annual_income']:,.2f}")
|
||||||
|
print(f"Final Principal (after 8 cycles): ${us_result['final_principal']:,.0f}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("CANADIAN LOTTERY - LOTTO 6/49 ($32M CAD)")
|
||||||
|
print("=" * 80)
|
||||||
|
can_result = calculate_canadian_lottery(32_000_000)
|
||||||
|
print(f"Original Jackpot (TAX FREE!): ${can_result['original_jackpot']:,.0f}")
|
||||||
|
print(f"Investment (90%): ${can_result['investment_principal']:,.0f}")
|
||||||
|
print(f"Fun Money (10%): ${can_result['fun_money']:,.0f}")
|
||||||
|
print(f"Daily Income: ${can_result['net_daily_income']:,.2f}")
|
||||||
|
print(f"Annual Income: ${can_result['annual_income']:,.2f}")
|
||||||
|
print(f"Final Principal (after 8 cycles): ${can_result['final_principal']:,.0f}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("COMPARISON")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"US ($547M) - You keep: ${us_result['net_amount_cad']:,.0f} CAD after taxes")
|
||||||
|
print(f"Canadian ($32M) - You keep: ${can_result['net_amount_cad']:,.0f} CAD (NO TAXES!)")
|
||||||
|
print(f"\nUS Daily Income: ${us_result['net_daily_income']:,.2f}")
|
||||||
|
print(f"Canadian Daily Income: ${can_result['net_daily_income']:,.2f}")
|
||||||
BIN
megamillions_debug.html
Normal file
BIN
megamillions_debug.html
Normal file
Binary file not shown.
5
powerball_numbers.html
Normal file
5
powerball_numbers.html
Normal file
File diff suppressed because one or more lines are too long
31
read_excel.py
Normal file
31
read_excel.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import openpyxl
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# Load the workbook
|
||||||
|
wb = openpyxl.load_workbook('Max.xlsx')
|
||||||
|
print(f"Sheets: {wb.sheetnames}\n")
|
||||||
|
|
||||||
|
# Read each sheet
|
||||||
|
for sheet_name in wb.sheetnames:
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"SHEET: {sheet_name}")
|
||||||
|
print('='*60)
|
||||||
|
ws = wb[sheet_name]
|
||||||
|
|
||||||
|
# Print first 30 rows
|
||||||
|
for i, row in enumerate(ws.iter_rows(values_only=True), 1):
|
||||||
|
if any(cell is not None for cell in row): # Skip completely empty rows
|
||||||
|
print(f"Row {i}: {row}")
|
||||||
|
if i >= 30:
|
||||||
|
break
|
||||||
|
|
||||||
|
print("\n\nNow using pandas for better formatting:")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# Try reading with pandas
|
||||||
|
for sheet_name in wb.sheetnames:
|
||||||
|
print(f"\n\nSheet: {sheet_name}")
|
||||||
|
print("-"*60)
|
||||||
|
df = pd.read_excel('Max.xlsx', sheet_name=sheet_name)
|
||||||
|
print(df.head(20))
|
||||||
|
print(f"\nColumns: {df.columns.tolist()}")
|
||||||
9
requirements.txt
Normal file
9
requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
flask
|
||||||
|
flask-cors
|
||||||
|
requests
|
||||||
|
beautifulsoup4
|
||||||
|
playwright
|
||||||
|
urllib3
|
||||||
|
openpyxl
|
||||||
|
pandas
|
||||||
|
schedule
|
||||||
27
scripts/bootstrap_repo.ps1
Normal file
27
scripts/bootstrap_repo.ps1
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$RepoPath
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$RootDir = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||||
|
$Target = (Resolve-Path $RepoPath).Path
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path (Join-Path $Target ".claude\agents") | Out-Null
|
||||||
|
New-Item -ItemType Directory -Force -Path (Join-Path $Target "SKILLS") | Out-Null
|
||||||
|
|
||||||
|
Copy-Item -Force (Join-Path $RootDir "AGENTS.md") (Join-Path $Target "AGENTS.md")
|
||||||
|
if (Test-Path (Join-Path $RootDir "SKILLS.md")) {
|
||||||
|
Copy-Item -Force (Join-Path $RootDir "SKILLS.md") (Join-Path $Target "SKILLS.md")
|
||||||
|
}
|
||||||
|
Copy-Item -Recurse -Force (Join-Path $RootDir "SKILLS\*") (Join-Path $Target "SKILLS")
|
||||||
|
Copy-Item -Recurse -Force (Join-Path $RootDir ".claude\agents\*") (Join-Path $Target ".claude\agents")
|
||||||
|
|
||||||
|
if (-not (Test-Path (Join-Path $Target ".gitlab-ci.yml")) -and (Test-Path (Join-Path $RootDir ".gitlab-ci.yml"))) {
|
||||||
|
Copy-Item -Force (Join-Path $RootDir ".gitlab-ci.yml") (Join-Path $Target ".gitlab-ci.yml")
|
||||||
|
}
|
||||||
|
if (-not (Test-Path (Join-Path $Target ".github")) -and (Test-Path (Join-Path $RootDir ".github"))) {
|
||||||
|
Copy-Item -Recurse -Force (Join-Path $RootDir ".github") (Join-Path $Target ".github")
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Bootstrapped repo: $Target"
|
||||||
|
Write-Host "Next: wire DoD gates to your stack and run scripts\dod.ps1"
|
||||||
33
scripts/bootstrap_repo.sh
Normal file
33
scripts/bootstrap_repo.sh
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Copy backbone files into an existing repo directory.
|
||||||
|
# Usage: ./scripts/bootstrap_repo.sh /path/to/repo
|
||||||
|
|
||||||
|
TARGET="${1:-}"
|
||||||
|
if [[ -z "$TARGET" ]]; then
|
||||||
|
echo "Usage: $0 /path/to/repo"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
mkdir -p "$TARGET/.claude/agents"
|
||||||
|
mkdir -p "$TARGET/SKILLS"
|
||||||
|
|
||||||
|
# Copy minimal backbone (adjust to taste)
|
||||||
|
cp -f "$SRC_DIR/AGENTS.md" "$TARGET/AGENTS.md"
|
||||||
|
cp -f "$SRC_DIR/SKILLS.md" "$TARGET/SKILLS.md" || true
|
||||||
|
cp -rf "$SRC_DIR/SKILLS/" "$TARGET/" || true
|
||||||
|
cp -rf "$SRC_DIR/.claude/agents/" "$TARGET/.claude/agents/" || true
|
||||||
|
|
||||||
|
# Optional: CI templates
|
||||||
|
if [[ ! -f "$TARGET/.gitlab-ci.yml" && -f "$SRC_DIR/.gitlab-ci.yml" ]]; then
|
||||||
|
cp -f "$SRC_DIR/.gitlab-ci.yml" "$TARGET/.gitlab-ci.yml"
|
||||||
|
fi
|
||||||
|
if [[ ! -d "$TARGET/.github" && -d "$SRC_DIR/.github" ]]; then
|
||||||
|
cp -rf "$SRC_DIR/.github" "$TARGET/.github"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Bootstrapped repo: $TARGET"
|
||||||
|
echo "Next: wire DoD gates to your stack (npm/pip) and run scripts/dod.sh"
|
||||||
42
scripts/dod.ps1
Normal file
42
scripts/dod.ps1
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
Write-Host "== DoD Gate =="
|
||||||
|
|
||||||
|
$root = Split-Path -Parent $PSScriptRoot
|
||||||
|
Set-Location $root
|
||||||
|
|
||||||
|
function Has-Command($name) {
|
||||||
|
return $null -ne (Get-Command $name -ErrorAction SilentlyContinue)
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasNode = Test-Path ".\package.json"
|
||||||
|
$hasPy = (Test-Path ".\pyproject.toml") -or (Test-Path ".\requirements.txt") -or (Test-Path ".\requirements-dev.txt")
|
||||||
|
|
||||||
|
if ($hasNode) {
|
||||||
|
if (-not (Has-Command "npm")) { throw "npm not found" }
|
||||||
|
Write-Host "+ npm ci"; npm ci
|
||||||
|
$pkg = Get-Content ".\package.json" | ConvertFrom-Json
|
||||||
|
if ($pkg.scripts.lint) { Write-Host "+ npm run lint"; npm run lint }
|
||||||
|
if ($pkg.scripts.typecheck) { Write-Host "+ npm run typecheck"; npm run typecheck }
|
||||||
|
if ($pkg.scripts.test) { Write-Host "+ npm test"; npm test }
|
||||||
|
if ($pkg.scripts.build) { Write-Host "+ npm run build"; npm run build }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasPy) {
|
||||||
|
if (-not (Has-Command "python")) { throw "python not found" }
|
||||||
|
Write-Host "+ python -m pip install -U pip"; python -m pip install -U pip
|
||||||
|
if (Test-Path ".\requirements.txt") { Write-Host "+ pip install -r requirements.txt"; pip install -r requirements.txt }
|
||||||
|
if (Test-Path ".\requirements-dev.txt") { Write-Host "+ pip install -r requirements-dev.txt"; pip install -r requirements-dev.txt }
|
||||||
|
if (Has-Command "ruff") {
|
||||||
|
Write-Host "+ ruff check ."; ruff check .
|
||||||
|
Write-Host "+ ruff format --check ."; ruff format --check .
|
||||||
|
}
|
||||||
|
if (Has-Command "pytest") { Write-Host "+ pytest -q"; pytest -q }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $hasNode -and -not $hasPy) {
|
||||||
|
Write-Host "No package.json or Python dependency files detected."
|
||||||
|
Write-Host "Customize scripts\dod.ps1 for this repo stack."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "DoD PASS"
|
||||||
43
scripts/dod.sh
Normal file
43
scripts/dod.sh
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "== DoD Gate =="
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
fail() { echo "DoD FAIL: $1" >&2; exit 1; }
|
||||||
|
run() { echo "+ $*"; "$@"; }
|
||||||
|
has() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
HAS_NODE=0
|
||||||
|
HAS_PY=0
|
||||||
|
[[ -f package.json ]] && HAS_NODE=1
|
||||||
|
[[ -f pyproject.toml || -f requirements.txt || -f requirements-dev.txt ]] && HAS_PY=1
|
||||||
|
|
||||||
|
if [[ $HAS_NODE -eq 1 ]]; then
|
||||||
|
has npm || fail "npm not found"
|
||||||
|
run npm ci
|
||||||
|
if has jq && jq -e '.scripts.lint' package.json >/dev/null 2>&1; then run npm run lint; fi
|
||||||
|
if has jq && jq -e '.scripts.typecheck' package.json >/dev/null 2>&1; then run npm run typecheck; fi
|
||||||
|
if has jq && jq -e '.scripts.test' package.json >/dev/null 2>&1; then run npm test; fi
|
||||||
|
if has jq && jq -e '.scripts.build' package.json >/dev/null 2>&1; then run npm run build; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $HAS_PY -eq 1 ]]; then
|
||||||
|
has python3 || fail "python3 not found"
|
||||||
|
run python3 -m pip install -U pip
|
||||||
|
if [[ -f requirements.txt ]]; then run python3 -m pip install -r requirements.txt; fi
|
||||||
|
if [[ -f requirements-dev.txt ]]; then run python3 -m pip install -r requirements-dev.txt; fi
|
||||||
|
if has ruff; then
|
||||||
|
run ruff check . || true
|
||||||
|
run ruff format --check . || true
|
||||||
|
fi
|
||||||
|
if has pytest; then run pytest -q || true; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $HAS_NODE -eq 0 && $HAS_PY -eq 0 ]]; then
|
||||||
|
echo "No package.json or Python dependency files detected."
|
||||||
|
echo "Customize scripts/dod.sh for this repo stack."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "DoD PASS"
|
||||||
56
scripts/monday.ps1
Normal file
56
scripts/monday.ps1
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
Param(
|
||||||
|
[Parameter(Mandatory=$false)][string]$Command = "status",
|
||||||
|
[Parameter(Mandatory=$false)][string]$RepoPath = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$RootDir = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||||
|
|
||||||
|
Write-Host "== Dev Backbone Monday Runner =="
|
||||||
|
|
||||||
|
function Need-Cmd($name) {
|
||||||
|
if (-not (Get-Command $name -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "Missing command: $name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($Command) {
|
||||||
|
"status" {
|
||||||
|
$code = (Get-Command code -ErrorAction SilentlyContinue)
|
||||||
|
$git = (Get-Command git -ErrorAction SilentlyContinue)
|
||||||
|
$docker = (Get-Command docker -ErrorAction SilentlyContinue)
|
||||||
|
|
||||||
|
Write-Host "[1] VS Code CLI:" ($code.Source ?? "NOT FOUND")
|
||||||
|
Write-Host "[2] Git: " ($git.Source ?? "NOT FOUND")
|
||||||
|
Write-Host "[3] Docker: " ($docker.Source ?? "NOT FOUND")
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Profiles expected: Dev, Cyber, Infra"
|
||||||
|
Write-Host "Try: code --list-extensions --profile Dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
"vscode-purge" {
|
||||||
|
Need-Cmd code
|
||||||
|
if ($env:CONFIRM -ne "YES") {
|
||||||
|
Write-Host "Refusing to uninstall extensions without CONFIRM=YES"
|
||||||
|
Write-Host "Run: `$env:CONFIRM='YES'; .\scripts\monday.ps1 -Command vscode-purge"
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
& (Join-Path $RootDir "scripts\vscode_profiles.ps1") -Action purge
|
||||||
|
}
|
||||||
|
|
||||||
|
"vscode-install" {
|
||||||
|
Need-Cmd code
|
||||||
|
& (Join-Path $RootDir "scripts\vscode_profiles.ps1") -Action install
|
||||||
|
}
|
||||||
|
|
||||||
|
"repo-bootstrap" {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($RepoPath)) {
|
||||||
|
throw "Usage: .\scripts\monday.ps1 -Command repo-bootstrap -RepoPath C:\path\to\repo"
|
||||||
|
}
|
||||||
|
& (Join-Path $RootDir "scripts\bootstrap_repo.ps1") -RepoPath $RepoPath
|
||||||
|
}
|
||||||
|
|
||||||
|
default {
|
||||||
|
throw "Unknown command: $Command"
|
||||||
|
}
|
||||||
|
}
|
||||||
66
scripts/monday.sh
Normal file
66
scripts/monday.sh
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Monday Overhaul Runner (safe by default)
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/monday.sh status
|
||||||
|
# ./scripts/monday.sh vscode-purge (requires CONFIRM=YES)
|
||||||
|
# ./scripts/monday.sh vscode-install
|
||||||
|
# ./scripts/monday.sh repo-bootstrap /path/to/repo
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# - VS Code profile creation is easiest once via UI (Profiles: Create Profile).
|
||||||
|
# This script assumes profiles exist: Dev, Cyber, Infra.
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
echo "== Dev Backbone Monday Runner =="
|
||||||
|
echo "Repo: $ROOT_DIR"
|
||||||
|
echo
|
||||||
|
|
||||||
|
cmd="${1:-status}"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || { echo "Missing command: $1"; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
status)
|
||||||
|
echo "[1] VS Code CLI: $(command -v code || echo 'NOT FOUND')"
|
||||||
|
echo "[2] Git: $(command -v git || echo 'NOT FOUND')"
|
||||||
|
echo "[3] Docker: $(command -v docker || echo 'NOT FOUND')"
|
||||||
|
echo
|
||||||
|
echo "Profiles expected: Dev, Cyber, Infra"
|
||||||
|
echo "Try: code --list-extensions --profile Dev"
|
||||||
|
;;
|
||||||
|
|
||||||
|
vscode-purge)
|
||||||
|
need_cmd code
|
||||||
|
if [[ "${CONFIRM:-NO}" != "YES" ]]; then
|
||||||
|
echo "Refusing to uninstall extensions without CONFIRM=YES"
|
||||||
|
echo "Run: CONFIRM=YES ./scripts/monday.sh vscode-purge"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
bash "$ROOT_DIR/scripts/vscode_profiles.sh" purge
|
||||||
|
;;
|
||||||
|
|
||||||
|
vscode-install)
|
||||||
|
need_cmd code
|
||||||
|
bash "$ROOT_DIR/scripts/vscode_profiles.sh" install
|
||||||
|
;;
|
||||||
|
|
||||||
|
repo-bootstrap)
|
||||||
|
repo_path="${1:-}"
|
||||||
|
if [[ -z "$repo_path" ]]; then
|
||||||
|
echo "Usage: ./scripts/monday.sh repo-bootstrap /path/to/repo"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
bash "$ROOT_DIR/scripts/bootstrap_repo.sh" "$repo_path"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $cmd"
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
75
scripts/vscode_profiles.ps1
Normal file
75
scripts/vscode_profiles.ps1
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
Param(
|
||||||
|
[Parameter(Mandatory=$true)][ValidateSet("purge","install")][string]$Action
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Profile-Exists([string]$ProfileName) {
|
||||||
|
try {
|
||||||
|
& code --list-extensions --profile $ProfileName | Out-Null
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Curated extension sets (edit to taste)
|
||||||
|
$DevExt = @(
|
||||||
|
"GitHub.copilot",
|
||||||
|
"GitHub.copilot-chat",
|
||||||
|
"GitHub.vscode-pull-request-github",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance",
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"ms-vscode-remote.remote-ssh",
|
||||||
|
"ms-vscode-remote.remote-containers",
|
||||||
|
"redhat.vscode-yaml",
|
||||||
|
"yzhang.markdown-all-in-one"
|
||||||
|
)
|
||||||
|
|
||||||
|
$CyberExt = @($DevExt) # add more only if needed
|
||||||
|
$InfraExt = @(
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"ms-vscode-remote.remote-ssh",
|
||||||
|
"redhat.vscode-yaml",
|
||||||
|
"yzhang.markdown-all-in-one"
|
||||||
|
)
|
||||||
|
|
||||||
|
function Purge-Profile([string]$ProfileName) {
|
||||||
|
Write-Host "Purging extensions from profile: $ProfileName"
|
||||||
|
if (-not (Profile-Exists $ProfileName)) {
|
||||||
|
Write-Host "Profile not found: $ProfileName (create once via UI: Profiles: Create Profile)"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$exts = & code --list-extensions --profile $ProfileName
|
||||||
|
foreach ($ext in $exts) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ext)) { continue }
|
||||||
|
& code --profile $ProfileName --uninstall-extension $ext | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-Profile([string]$ProfileName, [string[]]$Extensions) {
|
||||||
|
Write-Host "Installing extensions into profile: $ProfileName"
|
||||||
|
if (-not (Profile-Exists $ProfileName)) {
|
||||||
|
Write-Host "Profile not found: $ProfileName (create once via UI: Profiles: Create Profile)"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
foreach ($ext in $Extensions) {
|
||||||
|
& code --profile $ProfileName --install-extension $ext | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($Action) {
|
||||||
|
"purge" {
|
||||||
|
Purge-Profile "Dev"
|
||||||
|
Purge-Profile "Cyber"
|
||||||
|
Purge-Profile "Infra"
|
||||||
|
}
|
||||||
|
"install" {
|
||||||
|
Install-Profile "Dev" $DevExt
|
||||||
|
Install-Profile "Cyber" $CyberExt
|
||||||
|
Install-Profile "Infra" $InfraExt
|
||||||
|
}
|
||||||
|
}
|
||||||
93
scripts/vscode_profiles.sh
Normal file
93
scripts/vscode_profiles.sh
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Manage extensions per VS Code profile.
|
||||||
|
# Requires profiles to exist: Dev, Cyber, Infra
|
||||||
|
# Actions:
|
||||||
|
# ./scripts/vscode_profiles.sh purge (uninstall ALL extensions from those profiles)
|
||||||
|
# ./scripts/vscode_profiles.sh install (install curated sets)
|
||||||
|
|
||||||
|
ACTION="${1:-}"
|
||||||
|
if [[ -z "$ACTION" ]]; then
|
||||||
|
echo "Usage: $0 {purge|install}"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing: $1"; exit 1; }; }
|
||||||
|
need code
|
||||||
|
|
||||||
|
# Curated extension sets (edit to taste)
|
||||||
|
DEV_EXT=(
|
||||||
|
"GitHub.copilot"
|
||||||
|
"GitHub.copilot-chat"
|
||||||
|
"GitHub.vscode-pull-request-github"
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
"ms-python.python"
|
||||||
|
"ms-python.vscode-pylance"
|
||||||
|
"ms-azuretools.vscode-docker"
|
||||||
|
"ms-vscode-remote.remote-ssh"
|
||||||
|
"ms-vscode-remote.remote-containers"
|
||||||
|
"redhat.vscode-yaml"
|
||||||
|
"yzhang.markdown-all-in-one"
|
||||||
|
)
|
||||||
|
|
||||||
|
CYBER_EXT=(
|
||||||
|
"${DEV_EXT[@]}"
|
||||||
|
# Add only if you truly use them:
|
||||||
|
# "ms-kubernetes-tools.vscode-kubernetes-tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
INFRA_EXT=(
|
||||||
|
"ms-azuretools.vscode-docker"
|
||||||
|
"ms-vscode-remote.remote-ssh"
|
||||||
|
"redhat.vscode-yaml"
|
||||||
|
"yzhang.markdown-all-in-one"
|
||||||
|
# Optional:
|
||||||
|
# "hashicorp.terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
purge_profile() {
|
||||||
|
local profile="$1"
|
||||||
|
echo "Purging extensions from profile: $profile"
|
||||||
|
# list may fail if profile doesn't exist
|
||||||
|
if ! code --list-extensions --profile "$profile" >/dev/null 2>&1; then
|
||||||
|
echo "Profile not found: $profile (create once via UI: Profiles: Create Profile)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
code --list-extensions --profile "$profile" | while read -r ext; do
|
||||||
|
[[ -z "$ext" ]] && continue
|
||||||
|
code --profile "$profile" --uninstall-extension "$ext" || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
install_profile() {
|
||||||
|
local profile="$1"; shift
|
||||||
|
local exts=("$@")
|
||||||
|
echo "Installing extensions into profile: $profile"
|
||||||
|
if ! code --list-extensions --profile "$profile" >/dev/null 2>&1; then
|
||||||
|
echo "Profile not found: $profile (create once via UI: Profiles: Create Profile)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
for ext in "${exts[@]}"; do
|
||||||
|
[[ "$ext" =~ ^# ]] && continue
|
||||||
|
code --profile "$profile" --install-extension "$ext"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
purge)
|
||||||
|
purge_profile "Dev"
|
||||||
|
purge_profile "Cyber"
|
||||||
|
purge_profile "Infra"
|
||||||
|
;;
|
||||||
|
install)
|
||||||
|
install_profile "Dev" "${DEV_EXT[@]}"
|
||||||
|
install_profile "Cyber" "${CYBER_EXT[@]}"
|
||||||
|
install_profile "Infra" "${INFRA_EXT[@]}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown action: $ACTION"
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
137
send_email_now.py
Normal file
137
send_email_now.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
"""
|
||||||
|
Secure email sender that prompts for password instead of storing it.
|
||||||
|
This version is safer and works without App Passwords.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from email_sender import (
|
||||||
|
get_powerball,
|
||||||
|
get_mega_millions,
|
||||||
|
get_canadian_lotteries,
|
||||||
|
create_email_html,
|
||||||
|
format_currency
|
||||||
|
)
|
||||||
|
from datetime import datetime
|
||||||
|
import smtplib
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
def send_email_secure(sender_email, sender_password, recipient_email, subject, html_content):
|
||||||
|
"""Send email with provided credentials"""
|
||||||
|
try:
|
||||||
|
# Create message
|
||||||
|
msg = MIMEMultipart('alternative')
|
||||||
|
msg['Subject'] = subject
|
||||||
|
msg['From'] = sender_email
|
||||||
|
msg['To'] = recipient_email
|
||||||
|
|
||||||
|
# Attach HTML content
|
||||||
|
html_part = MIMEText(html_content, 'html')
|
||||||
|
msg.attach(html_part)
|
||||||
|
|
||||||
|
# Try Gmail first
|
||||||
|
try:
|
||||||
|
print(" Trying Gmail SMTP...")
|
||||||
|
with smtplib.SMTP('smtp.gmail.com', 587) as server:
|
||||||
|
server.starttls()
|
||||||
|
server.login(sender_email, sender_password)
|
||||||
|
server.send_message(msg)
|
||||||
|
print(f"✅ Email sent successfully via Gmail!")
|
||||||
|
return True
|
||||||
|
except Exception as gmail_error:
|
||||||
|
print(f" Gmail failed: {gmail_error}")
|
||||||
|
|
||||||
|
# Try alternative method - Gmail SSL port
|
||||||
|
try:
|
||||||
|
print(" Trying Gmail SSL (port 465)...")
|
||||||
|
with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server:
|
||||||
|
server.login(sender_email, sender_password)
|
||||||
|
server.send_message(msg)
|
||||||
|
print(f"✅ Email sent successfully via Gmail SSL!")
|
||||||
|
return True
|
||||||
|
except Exception as ssl_error:
|
||||||
|
print(f" Gmail SSL also failed: {ssl_error}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error sending email: {e}")
|
||||||
|
print("\n⚠️ Common issues:")
|
||||||
|
print(" 1. Gmail requires 2-Step Verification + App Password")
|
||||||
|
print(" 2. Check if 'Less secure app access' is enabled (not recommended)")
|
||||||
|
print(" 3. Verify your email and password are correct")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_lottery_email():
|
||||||
|
"""Fetch jackpots and send email with secure password prompt"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("🎰 LOTTERY JACKPOT EMAIL SENDER")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# Email configuration
|
||||||
|
sender_email = "mblanke@gmail.com"
|
||||||
|
recipient_email = "mblanke@gmail.com"
|
||||||
|
|
||||||
|
print(f"\n📧 Email will be sent from/to: {sender_email}")
|
||||||
|
print("\n🔐 Please enter your Gmail password:")
|
||||||
|
print(" (Note: Gmail may require an App Password if you have 2FA enabled)")
|
||||||
|
|
||||||
|
# Securely prompt for password (won't show on screen)
|
||||||
|
sender_password = getpass.getpass(" Password: ")
|
||||||
|
|
||||||
|
if not sender_password:
|
||||||
|
print("❌ No password provided. Exiting.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\n" + "-"*60)
|
||||||
|
print("📊 Fetching lottery jackpots...")
|
||||||
|
print("-"*60)
|
||||||
|
|
||||||
|
# Get US lotteries
|
||||||
|
print("\n🇺🇸 US Lotteries:")
|
||||||
|
print(" Fetching Powerball...")
|
||||||
|
powerball = get_powerball()
|
||||||
|
print(f" ✓ Powerball: {format_currency(powerball)}")
|
||||||
|
|
||||||
|
print(" Fetching Mega Millions...")
|
||||||
|
mega_millions = get_mega_millions()
|
||||||
|
print(f" ✓ Mega Millions: {format_currency(mega_millions)}")
|
||||||
|
|
||||||
|
# Get Canadian lotteries
|
||||||
|
print("\n🇨🇦 Canadian Lotteries:")
|
||||||
|
print(" Fetching Lotto Max and Lotto 6/49...")
|
||||||
|
lotto_max, lotto_649 = asyncio.run(get_canadian_lotteries())
|
||||||
|
print(f" ✓ Lotto Max: {format_currency(lotto_max)}")
|
||||||
|
print(f" ✓ Lotto 6/49: {format_currency(lotto_649)}")
|
||||||
|
|
||||||
|
# Create email
|
||||||
|
print("\n" + "-"*60)
|
||||||
|
print("📧 Creating email...")
|
||||||
|
print("-"*60)
|
||||||
|
subject = f"🎰 Lottery Report - {datetime.now().strftime('%B %d, %Y')}"
|
||||||
|
html_content = create_email_html(powerball, mega_millions, lotto_max, lotto_649)
|
||||||
|
print(" ✓ Email content created")
|
||||||
|
|
||||||
|
# Send email
|
||||||
|
print("\n📤 Sending email...")
|
||||||
|
success = send_email_secure(sender_email, sender_password, recipient_email, subject, html_content)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("✅ SUCCESS!")
|
||||||
|
print("="*60)
|
||||||
|
print(f"📧 Check your inbox at: {recipient_email}")
|
||||||
|
print("💡 The email includes all current jackpot amounts")
|
||||||
|
print(" with beautiful HTML formatting!")
|
||||||
|
else:
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("❌ FAILED!")
|
||||||
|
print("="*60)
|
||||||
|
print("\n🔧 Options to fix:")
|
||||||
|
print(" 1. Enable 2-Step Verification in Gmail")
|
||||||
|
print(" 2. Generate App Password: https://myaccount.google.com/apppasswords")
|
||||||
|
print(" 3. Use the App Password instead of regular password")
|
||||||
|
print("\n Alternative: Use a different email service (Outlook, Yahoo, etc.)")
|
||||||
|
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
send_lottery_email()
|
||||||
89
test_email.py
Normal file
89
test_email.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"""
|
||||||
|
Quick test script to send a lottery jackpot email immediately.
|
||||||
|
Use this to verify your email configuration before scheduling.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from email_sender import (
|
||||||
|
get_powerball,
|
||||||
|
get_mega_millions,
|
||||||
|
get_canadian_lotteries,
|
||||||
|
create_email_html,
|
||||||
|
send_email,
|
||||||
|
format_currency,
|
||||||
|
EMAIL_CONFIG
|
||||||
|
)
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def test_email():
|
||||||
|
"""Test the email sender by sending immediately"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("🧪 TESTING LOTTERY EMAIL SENDER")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# Display current configuration
|
||||||
|
print(f"\n📧 Email Configuration:")
|
||||||
|
print(f" From: {EMAIL_CONFIG['sender_email']}")
|
||||||
|
print(f" To: {EMAIL_CONFIG['recipient_email']}")
|
||||||
|
print(f" SMTP Server: {EMAIL_CONFIG['smtp_server']}:{EMAIL_CONFIG['smtp_port']}")
|
||||||
|
|
||||||
|
if EMAIL_CONFIG['sender_email'] == 'your-email@gmail.com':
|
||||||
|
print("\n⚠️ WARNING: You need to update EMAIL_CONFIG in email_sender.py!")
|
||||||
|
print(" Please edit the file and add your email credentials.")
|
||||||
|
print(" See EMAIL_SETUP.md for instructions.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\n" + "-"*60)
|
||||||
|
print("📊 Fetching lottery jackpots...")
|
||||||
|
print("-"*60)
|
||||||
|
|
||||||
|
# Get US lotteries
|
||||||
|
print("\n🇺🇸 US Lotteries:")
|
||||||
|
print(" Fetching Powerball...")
|
||||||
|
powerball = get_powerball()
|
||||||
|
print(f" ✓ Powerball: {format_currency(powerball)}")
|
||||||
|
|
||||||
|
print(" Fetching Mega Millions...")
|
||||||
|
mega_millions = get_mega_millions()
|
||||||
|
print(f" ✓ Mega Millions: {format_currency(mega_millions)}")
|
||||||
|
|
||||||
|
# Get Canadian lotteries
|
||||||
|
print("\n🇨🇦 Canadian Lotteries:")
|
||||||
|
print(" Fetching Lotto Max and Lotto 6/49...")
|
||||||
|
lotto_max, lotto_649 = asyncio.run(get_canadian_lotteries())
|
||||||
|
print(f" ✓ Lotto Max: {format_currency(lotto_max)}")
|
||||||
|
print(f" ✓ Lotto 6/49: {format_currency(lotto_649)}")
|
||||||
|
|
||||||
|
# Create email
|
||||||
|
print("\n" + "-"*60)
|
||||||
|
print("📧 Creating email...")
|
||||||
|
print("-"*60)
|
||||||
|
subject = f"🎰 TEST - Lottery Report - {datetime.now().strftime('%B %d, %Y')}"
|
||||||
|
html_content = create_email_html(powerball, mega_millions, lotto_max, lotto_649)
|
||||||
|
print(" ✓ Email content created")
|
||||||
|
|
||||||
|
# Send email
|
||||||
|
print("\n📤 Sending email...")
|
||||||
|
success = send_email(subject, html_content)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("✅ TEST SUCCESSFUL!")
|
||||||
|
print("="*60)
|
||||||
|
print(f"📧 Check your inbox at: {EMAIL_CONFIG['recipient_email']}")
|
||||||
|
print("💡 If everything looks good, you can run email_sender.py")
|
||||||
|
print(" to schedule daily emails at 7:00 AM")
|
||||||
|
else:
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("❌ TEST FAILED!")
|
||||||
|
print("="*60)
|
||||||
|
print("🔍 Troubleshooting tips:")
|
||||||
|
print(" 1. Check your email and password in EMAIL_CONFIG")
|
||||||
|
print(" 2. For Gmail, use an App Password (not your regular password)")
|
||||||
|
print(" 3. Verify SMTP server and port are correct")
|
||||||
|
print(" 4. Check your internet connection")
|
||||||
|
print(" 5. See EMAIL_SETUP.md for detailed instructions")
|
||||||
|
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_email()
|
||||||
Reference in New Issue
Block a user