From 4318c8f64277867f1a8c42445b0cc658b8751b22 Mon Sep 17 00:00:00 2001 From: mblanke Date: Tue, 10 Feb 2026 16:36:30 -0500 Subject: [PATCH] Initial commit with dev backbone template --- .dockerignore | 57 +++++ .github/copilot-instructions.md | 9 + .github/workflows/dod.yml | 17 ++ .gitignore | 25 ++ AGENTS.md | 78 ++++++ DOCKER_QUICKSTART.md | 255 +++++++++++++++++++ DOCKER_README.md | 347 ++++++++++++++++++++++++++ Dockerfile.backend | 57 +++++ Dockerfile.email | 46 ++++ Dockerfile.frontend | 47 ++++ EMAIL_SETUP.md | 208 +++++++++++++++ Max.xlsx | Bin 0 -> 11327 bytes SKILLS.md | 23 ++ SKILLS/00-operating-model.md | 21 ++ SKILLS/05-agent-taxonomy.md | 36 +++ SKILLS/10-definition-of-done.md | 24 ++ SKILLS/20-repo-map.md | 16 ++ SKILLS/25-algorithms-performance.md | 20 ++ SKILLS/26-vibe-coding-fundamentals.md | 31 +++ SKILLS/27-performance-profiling.md | 31 +++ SKILLS/30-implementation-rules.md | 16 ++ SKILLS/40-testing-quality.md | 14 ++ SKILLS/50-pr-review.md | 16 ++ SKILLS/56-ui-material-ui.md | 41 +++ SKILLS/60-security-safety.md | 15 ++ SKILLS/70-docs-artifacts.md | 13 + SKILLS/80-mcp-tools.md | 11 + SKILLS/82-mcp-server-design.md | 51 ++++ SKILLS/83-fastmcp-3-patterns.md | 40 +++ analyze_excel.py | 50 ++++ app.py | 172 +++++++++++++ docker-compose.prod.yml | 94 +++++++ docker-compose.yml | 59 +++++ docker-start.bat | 75 ++++++ docker-start.sh | 68 +++++ email_sender.py | 329 ++++++++++++++++++++++++ frontend | 1 + import requests.py | 186 ++++++++++++++ lottery_calculator.py | 195 +++++++++++++++ megamillions_debug.html | Bin 0 -> 14338 bytes powerball_numbers.html | 5 + read_excel.py | 31 +++ requirements.txt | 9 + scripts/bootstrap_repo.ps1 | 27 ++ scripts/bootstrap_repo.sh | 33 +++ scripts/dod.ps1 | 42 ++++ scripts/dod.sh | 43 ++++ scripts/monday.ps1 | 56 +++++ scripts/monday.sh | 66 +++++ scripts/vscode_profiles.ps1 | 75 ++++++ scripts/vscode_profiles.sh | 93 +++++++ send_email_now.py | 137 ++++++++++ test_email.py | 89 +++++++ 53 files changed, 3500 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/copilot-instructions.md create mode 100644 .github/workflows/dod.yml create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 DOCKER_QUICKSTART.md create mode 100644 DOCKER_README.md create mode 100644 Dockerfile.backend create mode 100644 Dockerfile.email create mode 100644 Dockerfile.frontend create mode 100644 EMAIL_SETUP.md create mode 100644 Max.xlsx create mode 100644 SKILLS.md create mode 100644 SKILLS/00-operating-model.md create mode 100644 SKILLS/05-agent-taxonomy.md create mode 100644 SKILLS/10-definition-of-done.md create mode 100644 SKILLS/20-repo-map.md create mode 100644 SKILLS/25-algorithms-performance.md create mode 100644 SKILLS/26-vibe-coding-fundamentals.md create mode 100644 SKILLS/27-performance-profiling.md create mode 100644 SKILLS/30-implementation-rules.md create mode 100644 SKILLS/40-testing-quality.md create mode 100644 SKILLS/50-pr-review.md create mode 100644 SKILLS/56-ui-material-ui.md create mode 100644 SKILLS/60-security-safety.md create mode 100644 SKILLS/70-docs-artifacts.md create mode 100644 SKILLS/80-mcp-tools.md create mode 100644 SKILLS/82-mcp-server-design.md create mode 100644 SKILLS/83-fastmcp-3-patterns.md create mode 100644 analyze_excel.py create mode 100644 app.py create mode 100644 docker-compose.prod.yml create mode 100644 docker-compose.yml create mode 100644 docker-start.bat create mode 100644 docker-start.sh create mode 100644 email_sender.py create mode 160000 frontend create mode 100644 import requests.py create mode 100644 lottery_calculator.py create mode 100644 megamillions_debug.html create mode 100644 powerball_numbers.html create mode 100644 read_excel.py create mode 100644 requirements.txt create mode 100644 scripts/bootstrap_repo.ps1 create mode 100644 scripts/bootstrap_repo.sh create mode 100644 scripts/dod.ps1 create mode 100644 scripts/dod.sh create mode 100644 scripts/monday.ps1 create mode 100644 scripts/monday.sh create mode 100644 scripts/vscode_profiles.ps1 create mode 100644 scripts/vscode_profiles.sh create mode 100644 send_email_now.py create mode 100644 test_email.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..02dd7f9 --- /dev/null +++ b/.dockerignore @@ -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/ diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..cab233c --- /dev/null +++ b/.github/copilot-instructions.md @@ -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. diff --git a/.github/workflows/dod.yml b/.github/workflows/dod.yml new file mode 100644 index 0000000..7cc778d --- /dev/null +++ b/.github/workflows/dod.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7d02af --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fb9c97a --- /dev/null +++ b/AGENTS.md @@ -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). diff --git a/DOCKER_QUICKSTART.md b/DOCKER_QUICKSTART.md new file mode 100644 index 0000000..d83d751 --- /dev/null +++ b/DOCKER_QUICKSTART.md @@ -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 /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 +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! 🎰🐋 diff --git a/DOCKER_README.md b/DOCKER_README.md new file mode 100644 index 0000000..e46fd7c --- /dev/null +++ b/DOCKER_README.md @@ -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. diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..92d60a1 --- /dev/null +++ b/Dockerfile.backend @@ -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"] diff --git a/Dockerfile.email b/Dockerfile.email new file mode 100644 index 0000000..4d28b59 --- /dev/null +++ b/Dockerfile.email @@ -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"] diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..066ba4e --- /dev/null +++ b/Dockerfile.frontend @@ -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"] diff --git a/EMAIL_SETUP.md b/EMAIL_SETUP.md new file mode 100644 index 0000000..f832d2b --- /dev/null +++ b/EMAIL_SETUP.md @@ -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 diff --git a/Max.xlsx b/Max.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..9099efb409585c030576434d692137810e36a18e GIT binary patch literal 11327 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 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`). diff --git a/SKILLS/30-implementation-rules.md b/SKILLS/30-implementation-rules.md new file mode 100644 index 0000000..978ce4b --- /dev/null +++ b/SKILLS/30-implementation-rules.md @@ -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. diff --git a/SKILLS/40-testing-quality.md b/SKILLS/40-testing-quality.md new file mode 100644 index 0000000..ed3c304 --- /dev/null +++ b/SKILLS/40-testing-quality.md @@ -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. diff --git a/SKILLS/50-pr-review.md b/SKILLS/50-pr-review.md new file mode 100644 index 0000000..3936f1a --- /dev/null +++ b/SKILLS/50-pr-review.md @@ -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 diff --git a/SKILLS/56-ui-material-ui.md b/SKILLS/56-ui-material-ui.md new file mode 100644 index 0000000..5bf519c --- /dev/null +++ b/SKILLS/56-ui-material-ui.md @@ -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. diff --git a/SKILLS/60-security-safety.md b/SKILLS/60-security-safety.md new file mode 100644 index 0000000..ecf24a9 --- /dev/null +++ b/SKILLS/60-security-safety.md @@ -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. diff --git a/SKILLS/70-docs-artifacts.md b/SKILLS/70-docs-artifacts.md new file mode 100644 index 0000000..a8720e3 --- /dev/null +++ b/SKILLS/70-docs-artifacts.md @@ -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 diff --git a/SKILLS/80-mcp-tools.md b/SKILLS/80-mcp-tools.md new file mode 100644 index 0000000..ef4e51b --- /dev/null +++ b/SKILLS/80-mcp-tools.md @@ -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. diff --git a/SKILLS/82-mcp-server-design.md b/SKILLS/82-mcp-server-design.md new file mode 100644 index 0000000..fdd5960 --- /dev/null +++ b/SKILLS/82-mcp-server-design.md @@ -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 diff --git a/SKILLS/83-fastmcp-3-patterns.md b/SKILLS/83-fastmcp-3-patterns.md new file mode 100644 index 0000000..5b49f61 --- /dev/null +++ b/SKILLS/83-fastmcp-3-patterns.md @@ -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. diff --git a/analyze_excel.py b/analyze_excel.py new file mode 100644 index 0000000..d674937 --- /dev/null +++ b/analyze_excel.py @@ -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}") diff --git a/app.py b/app.py new file mode 100644 index 0000000..81cd973 --- /dev/null +++ b/app.py @@ -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) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..29fe4b8 --- /dev/null +++ b/docker-compose.prod.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3033c76 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/docker-start.bat b/docker-start.bat new file mode 100644 index 0000000..8196726 --- /dev/null +++ b/docker-start.bat @@ -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 diff --git a/docker-start.sh b/docker-start.sh new file mode 100644 index 0000000..e25975b --- /dev/null +++ b/docker-start.sh @@ -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 "" diff --git a/email_sender.py b/email_sender.py new file mode 100644 index 0000000..94c72e4 --- /dev/null +++ b/email_sender.py @@ -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""" + + + + + +
+

🎰 Daily Lottery Jackpots

+ +
+

🇺🇸 US Lotteries

+
+
+ Powerball +
+ {format_currency(powerball)} +
+
+
+ Mega Millions +
+ {format_currency(mega_millions)} +
+
+ +
+

🇨🇦 Canadian Lotteries

+
+
+ Lotto Max + TAX FREE +
+ {format_currency(lotto_max)} +
+
+
+ Lotto 6/49 + TAX FREE +
+ {format_currency(lotto_649)} +
+
+ + + +
+ Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')} +
+
+ + + """ + 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() diff --git a/frontend b/frontend new file mode 160000 index 0000000..dcb2161 --- /dev/null +++ b/frontend @@ -0,0 +1 @@ +Subproject commit dcb2161ea4d5f89f48cb827469b066890ee0c1d9 diff --git a/import requests.py b/import requests.py new file mode 100644 index 0000000..a30766d --- /dev/null +++ b/import requests.py @@ -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']}") diff --git a/lottery_calculator.py b/lottery_calculator.py new file mode 100644 index 0000000..4c0e67d --- /dev/null +++ b/lottery_calculator.py @@ -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}") diff --git a/megamillions_debug.html b/megamillions_debug.html new file mode 100644 index 0000000000000000000000000000000000000000..3eec7937343fa74d47ffe1f14a83410f268d7050 GIT binary patch literal 14338 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ìG; zL3{S9ppeB|74h+X5L6Sq#Sfn205)xjA1pqcPLQvt!bfXUhM@(IuZ!)>CXE2)fTs6a zoA~O zxJ};?(^Ti`a9W641?yTvnrPjJS~@ij}?pByc+Ax-K2}DE=}wG z`o0tJ7(vWBzyoqy0nyDdDoP9vz$k>Nl+KpJ=<6E`KUxN5m~0g!NI%S6{P9Ny@IX~- z13??I71UpT zv<2lCaJ&W$1oMEbQ-FE)?*L=60rie)nn&40tUp0rWUnYoA~@hI?FfTUcak-5)8&^r zx4S<)`Q8JNrOUF?XB~0lO+G!G1uX?Tr`SUW}ML9Ln;z`UkKKo_}vgRG2%=p2`IVt_MB*p VLAi>wc-ZrFRykQY;Wvf0{|gmGGpYap literal 0 HcmV?d00001 diff --git a/powerball_numbers.html b/powerball_numbers.html new file mode 100644 index 0000000..36205db --- /dev/null +++ b/powerball_numbers.html @@ -0,0 +1,5 @@ +[ + { + "data": "\n\n\n\n\n \n\n\nPrevious Results | Powerball\n\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n \n \n \n \n \n \n \n \n\n \n \n \n\n\n\n \n \n\n \n\n\n\n\n\n\nSkip to content.\n\n\n\n\n
\n
\n

Previous Results

\n

Are you holding a winning ticket?

\n\n\n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n\n
\n \n
\n \n
\n
\n
\n
\n
Sat, Jul 19, 2025
\n
\n
\n\n
\n
\n
\n\n
28
\n\n
48
\n\n
51
\n\n
61
\n\n
69
\n\n
20
\n
\n
\n\n \n Power Play\n 3x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Wed, Jul 16, 2025
\n
\n
\n\n
\n
\n
\n\n
4
\n\n
21
\n\n
43
\n\n
48
\n\n
49
\n\n
22
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Mon, Jul 14, 2025
\n
\n
\n\n
\n
\n
\n\n
8
\n\n
12
\n\n
45
\n\n
46
\n\n
63
\n\n
24
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Sat, Jul 12, 2025
\n
\n
\n\n
\n
\n
\n\n
8
\n\n
16
\n\n
24
\n\n
33
\n\n
54
\n\n
18
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Wed, Jul 9, 2025
\n
\n
\n\n
\n
\n
\n\n
5
\n\n
9
\n\n
25
\n\n
28
\n\n
69
\n\n
5
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Mon, Jul 7, 2025
\n
\n
\n\n
\n
\n
\n\n
33
\n\n
35
\n\n
58
\n\n
61
\n\n
69
\n\n
25
\n
\n
\n\n \n Power Play\n 5x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Sat, Jul 5, 2025
\n
\n
\n\n
\n
\n
\n\n
1
\n\n
28
\n\n
34
\n\n
50
\n\n
58
\n\n
8
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Wed, Jul 2, 2025
\n
\n
\n\n
\n
\n
\n\n
7
\n\n
19
\n\n
21
\n\n
54
\n\n
63
\n\n
21
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Mon, Jun 30, 2025
\n
\n
\n\n
\n
\n
\n\n
13
\n\n
28
\n\n
44
\n\n
52
\n\n
55
\n\n
6
\n
\n
\n\n \n Power Play\n 4x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Sat, Jun 28, 2025
\n
\n
\n\n
\n
\n
\n\n
4
\n\n
35
\n\n
43
\n\n
52
\n\n
62
\n\n
12
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Wed, Jun 25, 2025
\n
\n
\n\n
\n
\n
\n\n
2
\n\n
12
\n\n
37
\n\n
51
\n\n
61
\n\n
22
\n
\n
\n\n \n Power Play\n 3x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Mon, Jun 23, 2025
\n
\n
\n\n
\n
\n
\n\n
5
\n\n
25
\n\n
42
\n\n
44
\n\n
65
\n\n
20
\n
\n
\n\n \n Power Play\n 3x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Sat, Jun 21, 2025
\n
\n
\n\n
\n
\n
\n\n
3
\n\n
16
\n\n
32
\n\n
52
\n\n
62
\n\n
24
\n
\n
\n\n \n Power Play\n 3x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Wed, Jun 18, 2025
\n
\n
\n\n
\n
\n
\n\n
23
\n\n
29
\n\n
50
\n\n
64
\n\n
67
\n\n
11
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Mon, Jun 16, 2025
\n
\n
\n\n
\n
\n
\n\n
17
\n\n
21
\n\n
23
\n\n
27
\n\n
52
\n\n
19
\n
\n
\n\n \n Power Play\n 5x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Sat, Jun 14, 2025
\n
\n
\n\n
\n
\n
\n\n
4
\n\n
6
\n\n
9
\n\n
23
\n\n
59
\n\n
25
\n
\n
\n\n \n Power Play\n 3x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Wed, Jun 11, 2025
\n
\n
\n\n
\n
\n
\n\n
13
\n\n
25
\n\n
29
\n\n
37
\n\n
53
\n\n
3
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Mon, Jun 9, 2025
\n
\n
\n\n
\n
\n
\n\n
30
\n\n
33
\n\n
40
\n\n
43
\n\n
52
\n\n
25
\n
\n
\n\n \n Power Play\n 4x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Sat, Jun 7, 2025
\n
\n
\n\n
\n
\n
\n\n
31
\n\n
36
\n\n
43
\n\n
48
\n\n
62
\n\n
25
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Wed, Jun 4, 2025
\n
\n
\n\n
\n
\n
\n\n
5
\n\n
17
\n\n
23
\n\n
35
\n\n
45
\n\n
24
\n
\n
\n\n \n Power Play\n 10x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Mon, Jun 2, 2025
\n
\n
\n\n
\n
\n
\n\n
1
\n\n
7
\n\n
44
\n\n
57
\n\n
61
\n\n
21
\n
\n
\n\n \n Power Play\n 3x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Sat, May 31, 2025
\n
\n
\n\n
\n
\n
\n\n
1
\n\n
29
\n\n
37
\n\n
56
\n\n
68
\n\n
13
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Wed, May 28, 2025
\n
\n
\n\n
\n
\n
\n\n
23
\n\n
27
\n\n
32
\n\n
35
\n\n
59
\n\n
11
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Mon, May 26, 2025
\n
\n
\n\n
\n
\n
\n\n
13
\n\n
47
\n\n
52
\n\n
64
\n\n
67
\n\n
25
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Sat, May 24, 2025
\n
\n
\n\n
\n
\n
\n\n
12
\n\n
18
\n\n
28
\n\n
48
\n\n
52
\n\n
5
\n
\n
\n\n \n Power Play\n 3x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Wed, May 21, 2025
\n
\n
\n\n
\n
\n
\n\n
9
\n\n
29
\n\n
31
\n\n
34
\n\n
43
\n\n
2
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Mon, May 19, 2025
\n
\n
\n\n
\n
\n
\n\n
13
\n\n
14
\n\n
37
\n\n
50
\n\n
60
\n\n
11
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Sat, May 17, 2025
\n
\n
\n\n
\n
\n
\n\n
7
\n\n
34
\n\n
40
\n\n
42
\n\n
52
\n\n
15
\n
\n
\n\n \n Power Play\n 2x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Wed, May 14, 2025
\n
\n
\n\n
\n
\n
\n\n
4
\n\n
10
\n\n
24
\n\n
29
\n\n
53
\n\n
4
\n
\n
\n\n \n Power Play\n 3x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n
\n
\n
Mon, May 12, 2025
\n
\n
\n\n
\n
\n
\n\n
15
\n\n
16
\n\n
41
\n\n
48
\n\n
60
\n\n
21
\n
\n
\n\n \n Power Play\n 3x\n \n
\n
\n
\n
\n\n
\n\n
\n \n \n \n
\n
\n
\n
\n \n
\n\n
\n
\n
\n\n\n\n\n\n
\n\n\n\n\n \n\n\n" + } +] \ No newline at end of file diff --git a/read_excel.py b/read_excel.py new file mode 100644 index 0000000..620f4d1 --- /dev/null +++ b/read_excel.py @@ -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()}") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a588b53 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +flask +flask-cors +requests +beautifulsoup4 +playwright +urllib3 +openpyxl +pandas +schedule diff --git a/scripts/bootstrap_repo.ps1 b/scripts/bootstrap_repo.ps1 new file mode 100644 index 0000000..7f77ba3 --- /dev/null +++ b/scripts/bootstrap_repo.ps1 @@ -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" diff --git a/scripts/bootstrap_repo.sh b/scripts/bootstrap_repo.sh new file mode 100644 index 0000000..64e1ac0 --- /dev/null +++ b/scripts/bootstrap_repo.sh @@ -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" diff --git a/scripts/dod.ps1 b/scripts/dod.ps1 new file mode 100644 index 0000000..3629973 --- /dev/null +++ b/scripts/dod.ps1 @@ -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" diff --git a/scripts/dod.sh b/scripts/dod.sh new file mode 100644 index 0000000..b672f49 --- /dev/null +++ b/scripts/dod.sh @@ -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" diff --git a/scripts/monday.ps1 b/scripts/monday.ps1 new file mode 100644 index 0000000..32eebfb --- /dev/null +++ b/scripts/monday.ps1 @@ -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" + } +} diff --git a/scripts/monday.sh b/scripts/monday.sh new file mode 100644 index 0000000..9f2f4f0 --- /dev/null +++ b/scripts/monday.sh @@ -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 diff --git a/scripts/vscode_profiles.ps1 b/scripts/vscode_profiles.ps1 new file mode 100644 index 0000000..aafefff --- /dev/null +++ b/scripts/vscode_profiles.ps1 @@ -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 + } +} diff --git a/scripts/vscode_profiles.sh b/scripts/vscode_profiles.sh new file mode 100644 index 0000000..2ed6497 --- /dev/null +++ b/scripts/vscode_profiles.sh @@ -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 diff --git a/send_email_now.py b/send_email_now.py new file mode 100644 index 0000000..510fddb --- /dev/null +++ b/send_email_now.py @@ -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() diff --git a/test_email.py b/test_email.py new file mode 100644 index 0000000..5728d89 --- /dev/null +++ b/test_email.py @@ -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()