diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61aa6c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +ENV/ +env/ +.venv + +# Flask +instance/ +.webassets-cache + +# Environment Variables +.env +.env.local +.env.*.local + +# Database +*.db +*.sqlite +*.sqlite3 + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +nohup.out + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Backups +*.backup +*.bak diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1de5767 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,97 @@ +# Changelog + +All notable changes to UrNetwork Stats Dashboard will be documented in this file. + +## [2.1.1] - 2024-11-21 + +### ✨ Added +- **Chart Interaction Mode** - Tooltips now show anywhere on chart, not just on data points +- Custom tooltip callbacks with formatted values (3 decimal places + GB unit) +- Better touch/mobile experience for charts +- Login button in header when not logged in + +### 🎨 Improved +- **Chart Text Visibility** - All text now white (#ffffff) instead of gray +- Chart axis labels now #e5e7eb for better readability +- Legend text is bold (weight 500) and larger (13px) +- Tooltip styling with dark background and blue border (#3b82f6) +- Login button styling (blue, prominent) + +### 🔧 Fixed +- **Daily Cleanup** - Changed from weekly (Sunday) to daily (3 AM) +- Keeps only last 7 days of data automatically +- Chart text no longer black/invisible on dark background + +### 📊 Technical +- Added `interaction.mode = 'index'` to all charts +- Added `interaction.intersect = false` for easier tooltip display +- Improved tooltip configuration with better colors and padding +- Login button CSS with `.login-btn` class + +--- + +## [2.1.0] - 2024-11-20 + +### 🎯 Major Changes +- **Clean Design** - Removed gradients, switched to solid Cloudflare-inspired colors +- Changed from purple (#667eea) to blue (#3b82f6) theme +- Solid background (#0c0d0e) instead of gradient +- Removed all glassmorphism/blur effects + +### 📊 Chart Improvements +- Better visibility with new color scheme +- Increased chart line width from 1px to 2px +- Better contrast for data visualization +- Optimized for dark theme readability + +### 🎨 Design Updates +- New stat cards without gradient backgrounds +- Simplified header design +- Cleaner button styles +- Better border colors (#2d3135) +- Improved text colors + +--- + +## [2.0.0] - 2024-11-15 + +### 🎯 Multi-Account Support (MAJOR UPDATE) +- Track unlimited UrNetwork accounts simultaneously +- Account management UI +- Individual charts per account +- Combined statistics +- Account nicknames +- Color-coded accounts + +### 🔐 Authentication Overhaul +- Separate admin password +- Secure session management +- Account-level authentication + +### 📊 Dashboard Enhancements +- React-powered private dashboard +- View modes (combined, paid/unpaid, delta) +- Chart visibility toggles +- Improved layout + +### 💾 Database Changes +- Multi-account schema +- Foreign key relationships +- Migration script from v1.0 +- Data preservation + +--- + +## [1.0.0] - 2024-10-01 + +### Initial Release +- Single account tracking +- Basic charts +- Public/private dashboard +- SQLite database +- Webhook support +- Web installer + +--- + +**Full documentation:** [README.md](README.md) diff --git a/CHART_INTERACTION.md b/CHART_INTERACTION.md new file mode 100644 index 0000000..d075c7d --- /dev/null +++ b/CHART_INTERACTION.md @@ -0,0 +1,233 @@ +# 🎯 Graf Tooltip Vylepšení - v2.1.1 + +## ✨ Co se změnilo + +### PŘED +``` +Tooltip se zobrazí JEN když najedeš přímo na datový bod (malý kolečko) +❌ Musíš přesně trefit bod +❌ Obtížné na mobilech +❌ Frustrující když je hodně bodů blízko sebe +``` + +### PO +``` +Tooltip se zobrazí KDEKOLIV na grafu +✅ Stačí najet myší kamkoliv na graf +✅ Automaticky najde nejbližší data +✅ Mnohem lepší UX! +``` + +--- + +## 🔧 Technické Řešení + +Přidal jsem **Chart.js interaction mode**: + +```javascript +interaction: { + mode: 'index', // Najde všechny datasety na indexu + intersect: false // Nemusíš trefit přesně bod +} +``` + +### Vysvětlení: +- **`mode: 'index'`** - Zobrazí tooltip pro všechny datasety na daném X indexu +- **`intersect: false`** - Tooltip se zobrazí i když nejsi přímo nad čárou + +--- + +## 📊 Jak to funguje + +### Single Dataset Graf (např. Total Data) +``` +Graf: ──────●──────●──────●────── + │ │ │ │ +Myš: │ ← zde → │ │ + │ │ │ │ + └─────┴──────┴──────┘ + Tooltip se ukáže u nejbližšího bodu +``` + +### Multi Dataset Graf (např. Paid vs Unpaid) +``` +Paid: ──────●──────●──────●────── +Unpaid: ──────●──────●──────●────── + │ │ │ │ +Myš: │ ← zde → │ │ + │ │ │ │ + +Tooltip ukáže: + Nov 9, 05:52 PM + ━━━━━━━━━━━━━━ + 💰 Paid: 1.234 GB + 📡 Unpaid: 0.567 GB +``` + +--- + +## 🎨 Bonus - Formátované Hodnoty + +Přidal jsem také custom callback pro lepší formátování: + +```javascript +callbacks: { + label: function(context) { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + if (context.parsed.y !== null) { + label += context.parsed.y.toFixed(3) + ' GB'; + } + return label; + } +} +``` + +**Výsledek:** +``` +// PŘED +Paid Data: 1.23456789 + +// PO +Paid Data: 1.235 GB ← Vždy 3 des. místa + jednotka +``` + +--- + +## 📱 Výhody + +### Desktop +✅ Rychlejší interakce - nemusíš lovit malé body +✅ Přesnější - ukazuje nejbližší hodnotu +✅ Plynulejší - tooltip plynule sleduje myš + +### Mobile/Touch +✅ Funguje s touch - stačí klepnout kamkoliv +✅ Větší "hit area" - není potřeba trefit malý bod +✅ Lepší UX na menších obrazovkách + +--- + +## 🔄 Kde to funguje + +Toto vylepšení je aplikováno na **VŠECHNY grafy**: + +### Veřejný Dashboard: +- ✅ Total Data Provided (GB) +- ✅ Individual Account Charts + +### Privátní Dashboard: +- ✅ Paid vs Unpaid Data +- ✅ Delta Chart +- ✅ Combined Chart +- ✅ Individual Account Charts + +--- + +## 🧪 Vyzkoušej + +1. **Otevři dashboard** +2. **Najdi graf** +3. **Jeď myší kdekoliv na grafu** (nemusíš trefit bod) +4. **Tooltip se okamžitě ukáže** s nejbližšími daty! + +### Test Multi-Dataset: +Na grafu s více čarami (Paid vs Unpaid): +- Jeď myší horizontálně přes graf +- Tooltip ukáže **obě** hodnoty najednou +- Vidíš data pro všechny datasety v daném čase + +--- + +## 💡 Pro Tipy + +### Rychlé Porovnání +Na multi-dataset grafu můžeš rychle porovnat hodnoty: +``` +Jeď myší zleva doprava → +Tooltip plynule ukazuje jak se mění obě hodnoty +``` + +### Přesné Hodnoty +I když jsou body blízko sebe: +``` +[●●●●●] ← Těsně u sebe +Tooltip vždy ukáže správný nejbližší bod +``` + +### Mobile +Na mobilech: +``` +Klepni kamkoliv na graf +→ Tooltip se ukáže +Jeď prstem po grafu +→ Tooltip sleduje tvůj prst +``` + +--- + +## 🎯 Interaction Modes + +Pro tvou informaci, Chart.js nabízí různé režimy: + +| Mode | Popis | Použití | +|------|-------|---------| +| `'point'` | Jen přesný bod | Původní chování ❌ | +| `'nearest'` | Nejbližší bod | Stále musíš být blízko | +| **`'index'`** | **Celý index** | **← Používáme! ✅** | +| `'dataset'` | Celý dataset | Pro srovnání | +| `'x'` / `'y'` | Podle osy | Specifické případy | + +**Proč `'index'`?** +- Nejlepší pro časové grafy +- Ukáže všechny datasety najednou +- Nejintuitivnější pro uživatele + +--- + +## 📊 Příklad Output + +### Single Chart: +``` +Nov 21, 01:37 PM +━━━━━━━━━━━━━━━━ +Total Data: 169.134 GB +``` + +### Multi Chart: +``` +Nov 21, 01:37 PM +━━━━━━━━━━━━━━━━ +💰 Paid (GB): 123.456 GB +📡 Unpaid (GB): 45.678 GB +``` + +--- + +## 🚀 Instalace + +Už je zahrnuté v **main_clean.py v2.1.1**! + +```bash +cd ~/urio +cp /path/to/main_clean.py main.py +pkill -f main.py +python3 main.py +``` + +--- + +## 📝 Changelog + +### v2.1.1 - 2024-11-21 +- ✅ Added `interaction.mode = 'index'` to all charts +- ✅ Added `interaction.intersect = false` for easier tooltip display +- ✅ Added custom tooltip callback for formatted values (3 decimal places) +- ✅ Improved mobile/touch experience +- ✅ Applied to both public and private dashboard charts + +--- + +**Výsledek: Mnohem lepší UX! Tooltip se ukáže kdekoliv najedeš myší na graf! 🎉** diff --git a/DESIGN_COMPARISON.md b/DESIGN_COMPARISON.md new file mode 100644 index 0000000..0c25aea --- /dev/null +++ b/DESIGN_COMPARISON.md @@ -0,0 +1,258 @@ +# 🎨 Design Update - Clean Cloudflare Style + +## 🔥 Co bylo opraveno + +### Problémy ve v2.0: +1. ❌ **Gradient pozadí** - tmavý fialový gradient byl příliš tmavý +2. ❌ **Fialové barvy** - grafy byly špatně viditelné +3. ❌ **Glassmorphism efekty** - příliš mnoho blur efektů +4. ❌ **Přemrštěné animace** - karty se příliš pohybovaly + +### ✅ Nový Clean Design: +1. ✅ **Čisté tmavé pozadí** - bez gradientu (#0c0d0e) +2. ✅ **Modré téma** - lépe viditelné grafy (#3b82f6) +3. ✅ **Minimální efekty** - čistý, profesionální vzhled +4. ✅ **Jemné animace** - subtilní hover efekty + +--- + +## 📥 Soubory + +### Hlavní Verze + +**[main_clean.py](computer:///mnt/user-data/outputs/main_clean.py)** - ✨ **NOVÁ ČISTÁ VERZE** +- Bez gradientu v pozadí +- Modrá barva místo fialové +- Lépe viditelné grafy +- Inspirováno Cloudflare dashboardem +- **Doporučeno k použití!** + +**[main_enhanced.py](computer:///mnt/user-data/outputs/main_enhanced.py)** - Původní v2.0 +- S gradientem a glassmorphism +- Fialové téma +- Více efektů + +--- + +## 🎨 Vizuální Srovnání + +### Barvy + +**Původní (v2.0 - Fialová):** +```css +--primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); +``` + +**Nová (Clean - Modrá):** +```css +--primary: #3b82f6; /* Čistá modrá */ +background: #0c0d0e; /* Bez gradientu */ +``` + +### Grafy + +**Původní:** +- Fialová čára: `#667eea` +- Slabá viditelnost na tmavém gradientu +- Málo kontrastu + +**Nová:** +- Modrá čára: `#3b82f6` +- Silnější bordura (2px místo 1px) +- Vyšší kontrast +- Lépe viditelné body na grafu + +### Karty + +**Původní:** +```css +background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, ...); +backdrop-filter: blur(10px); +transform: translateY(-5px); /* Příliš mnoho pohybu */ +``` + +**Nová:** +```css +background: #16181a; /* Čistá barva */ +/* Bez blur */ +/* Jemné hover efekty */ +``` + +--- + +## 🚀 Instalace Clean Verze + +### Pro NOVOU instalaci: +```bash +cd ~/urnetwork-stats + +# Stáhni main_clean.py +# Přejmenuj na main.py +mv main_clean.py main.py + +python3 main.py +``` + +### Pro UPGRADE z v2.0: +```bash +cd ~/urio + +# Zazálohuj současnou verzi +cp main.py main_gradient_backup.py + +# Nahraď čistou verzí +cp /path/to/main_clean.py main.py + +# Restartuj +pkill -f main.py +python3 main.py +``` + +--- + +## 🎯 Klíčové Změny v CSS + +### 1. Pozadí +```css +/* PŘED */ +body { + background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); + background-attachment: fixed; +} + +/* PO */ +body { + background: #0c0d0e; /* Čistá tmavá */ +} +``` + +### 2. Primární Barva +```css +/* PŘED */ +--primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + +/* PO */ +--primary: #3b82f6; /* Modrá Cloudflare style */ +``` + +### 3. Stat Karty +```css +/* PŘED */ +.stat-card { + background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); + backdrop-filter: blur(10px); +} +.stat-card:hover { + transform: translateY(-5px); +} + +/* PO */ +.stat-card { + background: #16181a; /* Čistý solid */ +} +.stat-card:hover { + border-color: #3b82f6; /* Jemný highlight */ +} +``` + +### 4. Grafy +```css +/* PŘED */ +borderColor: '#667eea', /* Fialová */ +backgroundColor: 'rgba(102, 126, 234, 0.2)', + +/* PO */ +borderColor: '#3b82f6', /* Modrá */ +backgroundColor: 'rgba(59, 130, 246, 0.15)', /* Světlejší pro kontrast */ +borderWidth: 2, /* Silnější čára */ +``` + +--- + +## 💡 Design Principy + +### Clean Design Philosophy: +1. **Méně je více** - Bez zbytečných efektů +2. **Čitelnost** - Vysoký kontrast, jasné barvy +3. **Konzistence** - Jednotný barevný systém +4. **Cloudflare inspirace** - Profesionální, čistý vzhled + +### Cloudflare Color System: +```css +Background: #0c0d0e (Hlavní pozadí) +Secondary BG: #16181a (Karty) +Card BG: #1a1d1f (Content boxy) +Border: #2d3135 (Okraje) +Primary: #3b82f6 (Modrá) +Text: #e5e7eb (Světlý text) +Text Muted: #9ca3af (Tlumený text) +``` + +--- + +## 🔄 Rollback na Gradient Verzi + +Pokud preferuješ původní gradient design: + +```bash +cd ~/urio + +# Použij original verzi +cp main_gradient_backup.py main.py + +# Nebo použij main_enhanced.py +cp /path/to/main_enhanced.py main.py + +python3 main.py +``` + +--- + +## 📊 Porovnání Výkonu + +### Rendering Speed: +- **Clean**: Rychlejší (bez blur efektů) +- **Gradient**: Pomalejší (backdrop-filter je náročný) + +### Čitelnost Grafů: +- **Clean**: ⭐⭐⭐⭐⭐ (Vynikající) +- **Gradient**: ⭐⭐⭐ (Průměrná) + +### Professional Look: +- **Clean**: ⭐⭐⭐⭐⭐ (Cloudflare style) +- **Gradient**: ⭐⭐⭐⭐ (Moderní, ale ne pro všechny) + +--- + +## 🎨 Customizace + +Chceš jiné barvy? Změň v main_clean.py: + +```css +:root { + --primary: #3b82f6; ← Změň na jinou barvu + /* Příklady: */ + /* Zelená: #10b981 */ + /* Fialová: #8b5cf6 */ + /* Červená: #ef4444 */ + /* Oranžová: #f59e0b */ +} +``` + +Vše ostatní se automaticky přizpůsobí! + +--- + +## ✅ Doporučení + +Pro většinu uživatelů doporučuji **main_clean.py**: +- ✅ Lépe viditelné grafy +- ✅ Čistší design +- ✅ Rychlejší rendering +- ✅ Profesionální vzhled +- ✅ Inspirováno Cloudflare + +--- + +**Vytvořeno s důrazem na čitelnost a profesionální vzhled! 🎉** diff --git a/DOWNLOAD_SUMMARY.md b/DOWNLOAD_SUMMARY.md new file mode 100644 index 0000000..6dd6585 --- /dev/null +++ b/DOWNLOAD_SUMMARY.md @@ -0,0 +1,313 @@ +# 🎉 UrNetwork Stats Dashboard v2.1.1 - Complete Package Ready! + +## ✅ Všechny Soubory Připraveny ke Stažení + +Kompletní balík souborů pro UrNetwork Stats Dashboard v2.1.1 je připraven! + +--- + +## 📦 Seznam Souborů (18 souborů) + +### 🐍 Hlavní Aplikace (2 soubory) + +1. **[main_clean.py](computer:///mnt/user-data/outputs/main_clean.py)** - 87 KB ⭐ **POUŽIJ TENTO!** + - Přejmenuj na `main.py` + - Clean Cloudflare design + - Bílý text na grafech + - Tooltip kdekoliv + - Denní cleanup + +2. **[main_enhanced.py](computer:///mnt/user-data/outputs/main_enhanced.py)** - 85 KB + - Alternativa s gradienty + - Fialové téma + - Volitelný + +--- + +### 📖 Dokumentace (12 souborů) + +3. **[README.md](computer:///mnt/user-data/outputs/README.md)** - 10 KB + - Hlavní anglický README + - Kompletní dokumentace + +4. **[README_CZ.md](computer:///mnt/user-data/outputs/README_CZ.md)** - 5.3 KB + - Český README + - Úplná česká dokumentace + +5. **[FILE_INDEX.md](computer:///mnt/user-data/outputs/FILE_INDEX.md)** - 8.1 KB ⭐ + - **ZAČNI ODTUD!** + - Přehled všech souborů + - Instalační návod + - Checklist + +6. **[QUICKSTART.md](computer:///mnt/user-data/outputs/QUICKSTART.md)** - 5.2 KB + - Rychlý start za 5 minut + +7. **[WEBHOOK_GUIDE.md](computer:///mnt/user-data/outputs/WEBHOOK_GUIDE.md)** - 7.9 KB + - 10+ webhook příkladů + - Discord, Slack, Telegram + +8. **[DESIGN_COMPARISON.md](computer:///mnt/user-data/outputs/DESIGN_COMPARISON.md)** - 5.3 KB + - Srovnání designů + - Proč tento design? + +9. **[CHART_INTERACTION.md](computer:///mnt/user-data/outputs/CHART_INTERACTION.md)** - 5.2 KB + - Tooltip vylepšení + - Jak to funguje + +10. **[FIXES_v2.1.md](computer:///mnt/user-data/outputs/FIXES_v2.1.md)** - 6.7 KB + - Všechny opravy v2.1 + +11. **[UPGRADE_GUIDE.md](computer:///mnt/user-data/outputs/UPGRADE_GUIDE.md)** - 6.7 KB + - Migrace z v1.0/v2.0 + +12. **[CHANGELOG.md](computer:///mnt/user-data/outputs/CHANGELOG.md)** - 2.7 KB + - Historie verzí + +13. **[FIX_INSTANCE_FOLDER.md](computer:///mnt/user-data/outputs/FIX_INSTANCE_FOLDER.md)** - 2.8 KB + - Fix pro starší instalace + +14. **[IMMEDIATE_FIX.md](computer:///mnt/user-data/outputs/IMMEDIATE_FIX.md)** - 3.1 KB + - Rychlé opravy + +--- + +### 🛠️ Instalační Soubory (4 soubory) + +15. **[requirements.txt](computer:///mnt/user-data/outputs/requirements.txt)** - 120 B + - Python závislosti + - Pro `pip install -r` + +16. **[install.sh](computer:///mnt/user-data/outputs/install.sh)** - 2.8 KB + - Automatický instalátor + - Bash script + +17. **[.gitignore](computer:///mnt/user-data/outputs/.gitignore)** - * + - Git ignore pravidla + +18. **[migrate.py](computer:///mnt/user-data/outputs/migrate.py)** - 11 KB + - Migrace z v1.0 + +--- + +## 🚀 Co Dělat Nyní + +### Krok 1: Stáhnout Všechny Soubory ⬇️ + +**Minimální sada (pro rychlý start):** +``` +✅ main_clean.py → přejmenuj na main.py +✅ requirements.txt +✅ FILE_INDEX.md → přečti FIRST! +✅ README.md nebo README_CZ.md +``` + +**Doporučená sada (kompletní):** +``` +✅ Všech 18 souborů +``` + +### Krok 2: Instalace 🔧 + +```bash +# 1. Vytvoř složku +mkdir urnetwork-stats +cd urnetwork-stats + +# 2. Stáhni soubory do této složky + +# 3. Přejmenuj hlavní soubor +mv main_clean.py main.py + +# 4. Spusť instalaci +chmod +x install.sh +./install.sh + +# NEBO manuálně: +pip3 install -r requirements.txt +mkdir -p instance + +# 5. Spusť +python3 main.py + +# 6. Otevři prohlížeč +# http://localhost:90 +``` + +### Krok 3: První Spuštění ⚙️ + +1. **Nastav admin heslo** (NENÍ UrNetwork heslo!) +2. **Přidej účty** - Menu → Správa Účtů +3. **Nastav webhooky** (volitelné) - Menu → Nastavení +4. **Hotovo!** 🎉 + +--- + +## 📋 Checklist Před Nahráním na Server/Repo + +- [ ] Všech 18 souborů staženo +- [ ] `main_clean.py` přejmenován na `main.py` +- [ ] Vytvořena složka `docs/` (volitelné) +- [ ] README.md v root složce +- [ ] requirements.txt v root složce +- [ ] install.sh má +x práva (`chmod +x install.sh`) +- [ ] .gitignore přítomen + +--- + +## 📂 Doporučená Struktura Po Stažení + +``` +urnetwork-stats/ # Root složka +├── main.py # ← Z main_clean.py +├── requirements.txt +├── install.sh +├── migrate.py +├── .gitignore +├── README.md +├── README_CZ.md +└── docs/ # Volitelná podsložka + ├── FILE_INDEX.md # ← Začni odtud! + ├── QUICKSTART.md + ├── WEBHOOK_GUIDE.md + ├── DESIGN_COMPARISON.md + ├── CHART_INTERACTION.md + ├── FIXES_v2.1.md + ├── UPGRADE_GUIDE.md + ├── CHANGELOG.md + ├── FIX_INSTANCE_FOLDER.md + └── IMMEDIATE_FIX.md +``` + +**Nebo jednodušeji (bez docs/ složky):** +``` +urnetwork-stats/ +├── main.py +├── requirements.txt +├── install.sh +├── .gitignore +├── README.md +└── (všechny ostatní .md soubory) +``` + +--- + +## 🎯 Priority Po Stažení + +### Musíš Přečíst (Povinné): +1. **[FILE_INDEX.md](computer:///mnt/user-data/outputs/FILE_INDEX.md)** ⭐ - Začni odtud! +2. **[README.md](computer:///mnt/user-data/outputs/README.md)** nebo **[README_CZ.md](computer:///mnt/user-data/outputs/README_CZ.md)** - Hlavní dokumentace + +### Měl bys Přečíst (Doporučené): +3. **[QUICKSTART.md](computer:///mnt/user-data/outputs/QUICKSTART.md)** - Rychlý start +4. **[WEBHOOK_GUIDE.md](computer:///mnt/user-data/outputs/WEBHOOK_GUIDE.md)** - Webhook setup + +### Můžeš Přečíst Později (Volitelné): +5. Ostatní `.md` soubory podle potřeby + +--- + +## 💾 Velikost Balíku + +**Celkem:** ~260 KB (všech 18 souborů) +- Aplikace (main.py): ~87 KB +- Dokumentace: ~70 KB +- Skripty: ~15 KB +- Ostatní: ~88 KB + +**Minimální sada:** ~97 KB +- main.py + requirements.txt + README.md + +--- + +## 🔄 Upgrade z Předchozí Verze? + +### Z v1.0 → v2.1: +1. Záloha `.env` a databáze +2. Stáhni nové soubory +3. Přejmenuj `main_clean.py` → `main.py` +4. Spusť `python3 migrate.py` +5. Restart + +### Z v2.0 → v2.1: +1. Záloha `main.py` +2. Nahraď `main_clean.py` → `main.py` +3. Restart + +📖 **Detaily:** [UPGRADE_GUIDE.md](computer:///mnt/user-data/outputs/UPGRADE_GUIDE.md) + +--- + +## 🐛 První Pomoc + +**Problém:** Port 90 obsazený +```python +# main.py, poslední řádek: +app.run(host="0.0.0.0", port=8080, debug=False) +``` + +**Problém:** Chybí závislosti +```bash +pip3 install -r requirements.txt +``` + +**Problém:** Nemůžu se přihlásit +```bash +echo "ADMIN_PASSWORD=tvoje_heslo" >> .env +``` + +**Problém:** Grafy bez textu +- Ujisti se, že používáš `main_clean.py` (ne `main_enhanced.py`) + +--- + +## 📞 Podpora + +- **Dokumentace:** Všechny `.md` soubory +- **Issues:** GitHub/Forgejo Issues +- **Quick Help:** [FILE_INDEX.md](computer:///mnt/user-data/outputs/FILE_INDEX.md) má FAQ + +--- + +## ✨ Co Je Nového v v2.1.1 + +- ✅ **Tooltip kdekoliv** na grafu (ne jen na bodech) +- ✅ **Bílý text** na grafech (konečně čitelný!) +- ✅ **Denní cleanup** (každý den ve 3:00) +- ✅ **Login button** v headeru +- ✅ Formátované hodnoty (3 des. místa) +- ✅ Better mobile/touch UX + +📖 **Detaily:** [FIXES_v2.1.md](computer:///mnt/user-data/outputs/FIXES_v2.1.md) + +--- + +## 🎉 Hotovo! + +**Máš všech 18 souborů připravených!** + +**Next Steps:** +1. ⬇️ Stáhni všechny soubory +2. 📖 Přečti [FILE_INDEX.md](computer:///mnt/user-data/outputs/FILE_INDEX.md) +3. 🚀 Spusť instalaci +4. 🎊 Užij si! + +--- + +**Made with ❤️ and 🤖 AI** + +**Version:** v2.1.1 +**Date:** November 21, 2024 +**Author:** Vlastík (mxnticek) + Claude (Anthropic) + +--- + +## 🔗 Quick Links + +- [FILE_INDEX.md](computer:///mnt/user-data/outputs/FILE_INDEX.md) - **Začni odtud!** +- [README.md](computer:///mnt/user-data/outputs/README.md) - Hlavní dokumentace +- [main_clean.py](computer:///mnt/user-data/outputs/main_clean.py) - Hlavní aplikace +- [requirements.txt](computer:///mnt/user-data/outputs/requirements.txt) - Závislosti +- [WEBHOOK_GUIDE.md](computer:///mnt/user-data/outputs/WEBHOOK_GUIDE.md) - Webhook návod + +**Stáhni vše a jsi ready to go! 🚀** diff --git a/FILE_INDEX.md b/FILE_INDEX.md new file mode 100644 index 0000000..14887d6 --- /dev/null +++ b/FILE_INDEX.md @@ -0,0 +1,367 @@ +# 📦 UrNetwork Stats Dashboard v2.1.1 - Complete Package + +## 📥 Download All Files + +Toto je kompletní balík souborů pro UrNetwork Stats Dashboard v2.1.1. + +--- + +## 📂 Hlavní Soubory + +### 🐍 Aplikace + +**[main_clean.py](computer:///mnt/user-data/outputs/main_clean.py)** - ⭐ **HLAVNÍ SOUBOR** +- Kompletní Flask aplikace +- Multi-account podpora +- Čistý Cloudflare design +- Všechny opravy v2.1.1 +- **Přejmenuj na `main.py` při instalaci!** +- Velikost: ~88 KB + +**[main_enhanced.py](computer:///mnt/user-data/outputs/main_enhanced.py)** - Alternativa s gradienty +- Verze s fialovým gradientem a glassmorphismem +- Pro ty, kdo preferují původní v2.0 design +- Velikost: ~86 KB + +--- + +### 📖 Dokumentace + +**[README.md](computer:///mnt/user-data/outputs/README.md)** - Hlavní anglický README +- Kompletní dokumentace projektu +- Quick start guide +- Webhook příklady +- Troubleshooting + +**[README_CZ.md](computer:///mnt/user-data/outputs/README_CZ.md)** - Český README +- Úplná česká dokumentace +- Rychlý start +- Webhook návod + +**[QUICKSTART.md](computer:///mnt/user-data/outputs/QUICKSTART.md)** - Rychlý start +- Instalace za 5 minut +- První spuštění +- Základní konfigurace + +**[WEBHOOK_GUIDE.md](computer:///mnt/user-data/outputs/WEBHOOK_GUIDE.md)** - Webhook návod +- 10+ webhook příkladů +- Discord, Slack, Telegram +- Dostupné proměnné +- Debugging tipy + +**[DESIGN_COMPARISON.md](computer:///mnt/user-data/outputs/DESIGN_COMPARISON.md)** - Design dokumentace +- Srovnání v2.0 vs v2.1 +- Důvody změn +- Customizace barev + +**[CHART_INTERACTION.md](computer:///mnt/user-data/outputs/CHART_INTERACTION.md)** - Graf interakce +- Tooltip kdekoliv na grafu +- Jak to funguje +- Mobile optimalizace + +**[FIXES_v2.1.md](computer:///mnt/user-data/outputs/FIXES_v2.1.md)** - Seznam oprav v2.1 +- Všechny opravy +- Před/po srovnání +- Technické detaily + +**[UPGRADE_GUIDE.md](computer:///mnt/user-data/outputs/UPGRADE_GUIDE.md)** - Upgrade průvodce +- Migrace z v1.0 +- Upgrade z v2.0 +- Backup strategie + +**[CHANGELOG.md](computer:///mnt/user-data/outputs/CHANGELOG.md)** - Historie změn +- Všechny verze +- Co je nového +- Breaking changes + +--- + +### 🛠️ Instalační Soubory + +**[requirements.txt](computer:///mnt/user-data/outputs/requirements.txt)** - Python závislosti +```txt +Flask==3.0.0 +Flask-SQLAlchemy==3.1.1 +Flask-APScheduler==1.13.1 +requests==2.31.0 +python-dateutil==2.8.2 +gunicorn==21.2.0 +``` + +**[install.sh](computer:///mnt/user-data/outputs/install.sh)** - Automatický instalační script +- Kontrola závislostí +- Instalace balíčků +- Vytvoření složek +- Backup existujících dat + +**[.gitignore](computer:///mnt/user-data/outputs/.gitignore)** - Git ignore file +- Python cache +- Virtual environments +- Database files +- Environment variables + +--- + +### 🔧 Utility Skripty + +**[migrate.py](computer:///mnt/user-data/outputs/migrate.py)** - Migrace z v1.0 +- Automatický upgrade na v2.0 +- Zachování dat +- Backup tvorba + +**[fix_db_path.py](computer:///mnt/user-data/outputs/fix_db_path.py)** - Fix databázové cesty +- Oprava instance folder problémů +- Pro starší instalace + +--- + +## 🚀 Rychlá Instalace + +### Metoda 1: Automatická (Doporučeno) + +```bash +# 1. Stáhni všechny soubory do složky +cd urnetwork-stats + +# 2. Přejmenuj main_clean.py na main.py +mv main_clean.py main.py + +# 3. Spusť instalační script +chmod +x install.sh +./install.sh + +# 4. Spusť aplikaci +python3 main.py +``` + +### Metoda 2: Manuální + +```bash +# 1. Stáhni soubory +cd urnetwork-stats + +# 2. Přejmenuj +mv main_clean.py main.py + +# 3. Instaluj závislosti +pip3 install -r requirements.txt + +# 4. Vytvoř složky +mkdir -p instance + +# 5. Spusť +python3 main.py +``` + +### Metoda 3: S Gunicorn (Produkce) + +```bash +# Po instalaci závislostí +gunicorn --bind 0.0.0.0:90 --workers 4 main:app +``` + +--- + +## 📋 Checklist Po Stažení + +- [ ] Stáhnout všechny soubory +- [ ] Přejmenovat `main_clean.py` → `main.py` +- [ ] Spustit `install.sh` nebo manuální instalaci +- [ ] Otevřít `http://localhost:90` +- [ ] Nastavit admin heslo +- [ ] Přidat UrNetwork účty +- [ ] (Volitelné) Nastavit webhooky +- [ ] (Volitelné) Přidat do systemd + +--- + +## 📊 Struktura Po Instalaci + +``` +urnetwork-stats/ +├── main.py # Hlavní aplikace (z main_clean.py) +├── main_enhanced.py # Alternativa (volitelné) +├── requirements.txt # Závislosti +├── install.sh # Instalační script +├── migrate.py # Migrace script +├── .gitignore # Git ignore +├── .env # Config (vytvoří se automaticky) +├── instance/ +│ └── transfer_stats.db # Databáze (vytvoří se automaticky) +└── docs/ + ├── README.md + ├── README_CZ.md + ├── QUICKSTART.md + ├── WEBHOOK_GUIDE.md + ├── DESIGN_COMPARISON.md + ├── CHART_INTERACTION.md + ├── FIXES_v2.1.md + ├── UPGRADE_GUIDE.md + └── CHANGELOG.md +``` + +--- + +## 🎯 Co Potřebuješ Minimálně + +Pro základní funkčnost: + +1. **Povinné:** + - `main_clean.py` (přejmenuj na `main.py`) + - `requirements.txt` + +2. **Doporučené:** + - `README.md` nebo `QUICKSTART.md` + - `install.sh` (usnadní instalaci) + +3. **Volitelné:** + - Všechny ostatní `.md` soubory (dokumentace) + - `main_enhanced.py` (alternativní design) + - `migrate.py` (jen pokud upgraduješ z v1.0) + +--- + +## 🔄 Upgrade z Předchozí Verze + +### Z v1.0 (Single Account) + +```bash +# 1. Záloha +cp .env .env.backup +cp instance/transfer_stats.db instance/transfer_stats.db.backup + +# 2. Stáhnout nové soubory +# (všechny soubory z tohoto balíku) + +# 3. Přejmenovat +mv main_clean.py main.py + +# 4. Spustit migraci +python3 migrate.py + +# 5. Restart +python3 main.py +``` + +### Z v2.0 (Gradient Design) + +```bash +# Jednoduše nahraď main.py +cp main.py main.py.v2.0.backup +mv main_clean.py main.py +python3 main.py +``` + +--- + +## 📖 První Kroky Po Instalaci + +1. **Otevři prohlížeč:** `http://localhost:90` +2. **Nastav admin heslo:** (NENÍ to tvé UrNetwork heslo!) +3. **Přidej účty:** + - Menu → Správa Účtů + - Přidat Účet + - Zadej UrNetwork email, heslo, přezdívku +4. **Nastav webhooky (volitelné):** + - Menu → Nastavení + - Webhook Management + - Přidej Discord/Slack/Telegram URL + payload +5. **Hotovo!** Dashboard začne sbírat data každých 15 minut + +--- + +## 🐛 Časté Problémy + +### Port 90 je obsazený +```python +# Změň v main.py poslední řádek: +app.run(host="0.0.0.0", port=8080, debug=False) +``` + +### Chybí závislosti +```bash +pip3 install Flask Flask-SQLAlchemy Flask-APScheduler requests python-dateutil +``` + +### Nemůžu se přihlásit +```bash +# Zkontroluj .env +cat .env | grep ADMIN_PASSWORD +# Pokud chybí: +echo "ADMIN_PASSWORD=tvoje_heslo" >> .env +``` + +### Grafy neukazují text +- Ujisti se, že používáš `main_clean.py` (ne `main_enhanced.py`) +- Verze musí být v2.1+ + +--- + +## 💡 Pro Tipy + +### Rychlé Testování +```bash +# Spusť bez instalace systemd +python3 main.py + +# V produkci použij Gunicorn +gunicorn --bind 0.0.0.0:90 main:app +``` + +### Vývoj +```bash +# Debug mode +python3 main.py # Debug je defaultně vypnutý + +# Pro development změň v main.py: +app.run(host="0.0.0.0", port=90, debug=True) +``` + +### Backup +```bash +# Před každým upgradem +cp .env .env.backup +cp instance/transfer_stats.db instance/transfer_stats.db.backup +``` + +--- + +## 📞 Podpora + +- **Issues:** Forgejo Issues +- **Dokumentace:** Všechny `.md` soubory v balíku +- **Discord:** (pokud máš komunitní server) + +--- + +## ✅ Závěrečný Checklist + +Před nahráním na server/repo zkontroluj: + +- [ ] Všechny soubory staženy +- [ ] `main_clean.py` přejmenován na `main.py` +- [ ] `requirements.txt` přítomen +- [ ] `README.md` přítomen +- [ ] `install.sh` má +x práva +- [ ] `.gitignore` přítomen +- [ ] Dokumentace v `docs/` (volitelné) + +--- + +## 🎉 Hotovo! + +Máš kompletní balík souborů pro UrNetwork Stats Dashboard v2.1.1! + +**Co dělat dál:** +1. Stáhni všechny soubory +2. Nahraj na server/do repozitáře +3. Spusť instalaci +4. Užij si! + +**Made with ❤️ and 🤖 AI** + +--- + +**Verze:** v2.1.1 +**Datum:** 21. listopadu 2024 +**Autor:** Vlastík (mxnticek) + Claude (Anthropic) diff --git a/FIXES_v2.1.md b/FIXES_v2.1.md new file mode 100644 index 0000000..ce32e6a --- /dev/null +++ b/FIXES_v2.1.md @@ -0,0 +1,287 @@ +# ✅ Opravy v main_clean.py - Finální Verze + +## 🎯 Co bylo opraveno + +### 1. ✅ Grafy - Viditelný Text +**Problém:** Text na grafech byl černý a skoro neviditelný na tmavém pozadí + +**Oprava:** +```javascript +// PŘED - Špatně viditelný +ticks: { color: 'var(--text-muted)' } // Tmavě šedá +legend: { labels: { color: 'var(--text-color)' } } // Není dostatečně světlá + +// PO - Jasně viditelný ✅ +ticks: { color: '#e5e7eb', font: { size: 12 } } // Světle bílá +legend: { + labels: { + color: '#ffffff', // Čistě bílá + font: { size: 13, weight: '500' }, + padding: 15 + } +} +``` + +**Vylepšení:** +- ✅ Osy (X, Y) - bílý text místo šedého +- ✅ Legenda - bílý tučný text +- ✅ Tooltips - tmavé pozadí s modrým rámečkem +- ✅ Větší velikost písma pro lepší čitelnost + +--- + +### 2. ✅ Login Tlačítko v Headeru +**Problém:** Nebylo jasné, kde se přihlásit, když nejsi přihlášený + +**Oprava:** +```html + +{% if session.logged_in %} + +{% else %} + Přihlásit se +{% endif %} +``` + +**CSS:** +```css +.header-nav a.login-btn { + background: #3b82f6; /* Modré pozadí */ + color: white; + font-weight: 600; /* Tučné */ +} +``` + +**Výsledek:** +- ✅ Viditelné modré tlačítko "Přihlásit se" v headeru +- ✅ Zobrazuje se pouze když nejsi přihlášený +- ✅ Zmizí po přihlášení + +--- + +### 3. ✅ Automatické Mazání Starých Dat +**Problém:** Data se mazala jen jednou týdně v neděli + +**Oprava:** +```python +# PŘED - Jednou týdně +@scheduler.task(trigger="cron", day_of_week="sun", hour="0") + +# PO - Každý den ✅ +@scheduler.task(trigger="cron", hour="3", minute="0") +``` + +**Jak to funguje:** +1. **Každý den ve 3:00 ráno** se spustí cleanup job +2. Smaže všechny záznamy **starší než 7 dní** +3. Ponechá data z **posledního týdne** +4. Loguje kolik záznamů bylo smazáno + +**Příklad:** +``` +Dnes je: 21. listopadu 2024 +Smaže se: Vše před 14. listopadem 2024 +Zůstane: 14. - 21. listopadu (poslední 7 dní) +``` + +--- + +## 📊 Před & Po - Vizuální Srovnání + +### Grafy - Text +``` +PŘED: +┌────────────────────────┐ +│ Graf │ +│ (text skoro neviditelný)│ +│ Osa Y: #9ca3af (tmavá) │ +│ Osa X: #9ca3af (tmavá) │ +│ Legenda: #d1d5db │ +└────────────────────────┘ + +PO: +┌────────────────────────┐ +│ Graf │ +│ (text jasně viditelný!) │ +│ Osa Y: #e5e7eb (světlá)│ +│ Osa X: #e5e7eb (světlá)│ +│ Legenda: #ffffff (bílá)│ +└────────────────────────┘ +``` + +### Header +``` +PŘED: +[Veřejný pohled] (když nejsi přihlášený, není jasné kde se přihlásit) + +PO: +[Veřejný pohled] [🔵 Přihlásit se] (jasné modré tlačítko) +``` + +### Cleanup Schedule +``` +PŘED: +Neděle 00:00 → Smaže data starší 7 dní + +PO: +Každý den 03:00 → Smaže data starší 7 dní +``` + +--- + +## 🚀 Instalace Opravené Verze + +```bash +cd ~/urio + +# Zazálohuj současnou verzi +cp main.py main.py.before_fixes + +# Nahraď opravenou verzí +cp /path/to/main_clean.py main.py + +# Restartuj +pkill -f main.py +python3 main.py +``` + +--- + +## ✨ Co se vylepšilo + +### Čitelnost Grafů ⭐⭐⭐⭐⭐ +- **PŘED**: ⭐⭐ (Text skoro neviditelný) +- **PO**: ⭐⭐⭐⭐⭐ (Jasně čitelné vše!) + +### UX - Přihlášení ⭐⭐⭐⭐⭐ +- **PŘED**: ⭐⭐⭐ (Musíš znát URL /login) +- **PO**: ⭐⭐⭐⭐⭐ (Jasné modré tlačítko v headeru) + +### Údržba Databáze ⭐⭐⭐⭐⭐ +- **PŘED**: ⭐⭐⭐⭐ (Týdně - může se hromadit) +- **PO**: ⭐⭐⭐⭐⭐ (Denně - vždy jen poslední týden) + +--- + +## 🎨 Technické Detaily + +### Chart.js Konfigurace +```javascript +// Kompletní chart options pro viditelný text +{ + scales: { + y: { + ticks: { + color: '#e5e7eb', // Světlá barva + font: { size: 12 } // Větší písmo + }, + grid: { + color: 'rgba(45, 49, 53, 0.3)' // Jemná mřížka + } + }, + x: { + ticks: { + color: '#e5e7eb', + font: { size: 11 } + } + } + }, + plugins: { + legend: { + labels: { + color: '#ffffff', // Bílá + font: { size: 13, weight: '500' }, + padding: 15 + } + }, + tooltip: { + backgroundColor: 'rgba(26, 29, 31, 0.95)', // Tmavé pozadí + titleColor: '#ffffff', + bodyColor: '#e5e7eb', + borderColor: '#3b82f6', + borderWidth: 1, + padding: 12 + } + } +} +``` + +### Cleanup Job +```python +@scheduler.task( + id="cleanup_old_stats_job", + trigger="cron", + hour="3", # Každý den ve 3:00 + minute="0" +) +def cleanup_old_stats_job(): + cutoff_date = datetime.datetime.now() - datetime.timedelta(days=7) + # Smaže vše starší než cutoff_date + db.session.query(Stats).filter(Stats.timestamp < cutoff_date).delete() +``` + +--- + +## 💾 Velikost Databáze + +S denním čištěním: +``` +Stats každých 15 min = 96 záznamů/den +7 dní × 96 = 672 záznamů max +3 účty × 672 = ~2000 záznamů max + +Velikost: ~200 KB databáze (velmi malá!) +``` + +Bez čištění (týdně): +``` +Po měsíci: ~8000 záznamů +Velikost: ~800 KB (pořád OK, ale zbytečné) +``` + +--- + +## 🔍 Ověření Změn + +### 1. Grafy +``` +Otevři dashboard → Podívej se na graf +✅ Čísla na osách jsou bílá a čitelná +✅ Legenda je bílá a tučná +✅ Tooltips mají modré ohraničení +``` + +### 2. Login Tlačítko +``` +Odhlásit se (pokud jsi přihlášený) +✅ V headeru se objeví modré tlačítko "Přihlásit se" +Přihlásit se +✅ Tlačítko zmizí, objeví se menu +``` + +### 3. Cleanup Job +```bash +# Zkontroluj logy +tail -f nohup.out | grep cleanup + +# Mělo by se objevit každý den ve 3:00 +# "Running daily stats cleanup job..." +# "Successfully deleted X stats records older than 7 days." +``` + +--- + +## 📝 Changelog + +### v2.1 - 2024-11-21 +- ✅ Fixed chart text visibility (white text instead of dark gray) +- ✅ Added Login button to header when not logged in +- ✅ Changed cleanup schedule from weekly to daily (3 AM) +- ✅ Enhanced tooltip styling with blue borders +- ✅ Increased font sizes for better readability + +--- + +**Všechno opraveno a připraveno k použití! 🎉** + +Grafy jsou nyní perfektně čitelné, login je viditelný a databáze se čistí každý den automaticky! diff --git a/FIX_INSTANCE_FOLDER.md b/FIX_INSTANCE_FOLDER.md new file mode 100644 index 0000000..8466894 --- /dev/null +++ b/FIX_INSTANCE_FOLDER.md @@ -0,0 +1,113 @@ +# 🔧 Fix: Databáze v instance/ složce + +## Problém +Pokud vidíš chybu: +``` +✗ Databáze transfer_stats.db nenalezena! +``` + +Ale databáze je ve složce `instance/`: +```bash +ls instance/ +# transfer_stats.db +``` + +## Řešení + +Opravený migrační skript již automaticky hledá databázi v těchto lokacích: +- `transfer_stats.db` (root složka) +- `instance/transfer_stats.db` (Flask standardní umístění) +- `../transfer_stats.db` (parent folder) + +### Použij aktualizované soubory + +Právě jsem opravil oba soubory: +1. **migrate.py** - Nyní hledá DB v instance/ +2. **main_enhanced.py** - Používá správnou cestu `sqlite:///instance/transfer_stats.db` + +### Rychlé řešení + +```bash +# 1. Stáhni znovu opravené soubory z /mnt/user-data/outputs/ +# 2. Překopíruj je do ~/urio/ +cd ~/urio + +# 3. Zkus migraci znovu +python3 migrate.py +``` + +## Alternativní řešení (pokud potřebuješ hned) + +Pokud chceš použít stávající soubory bez stahování nových: + +```bash +cd ~/urio + +# Přesuň databázi do root složky +cp instance/transfer_stats.db . + +# Spusť migraci +python3 migrate.py + +# Po úspěšné migraci, databáze zůstane v root složce +# nebo ji vrať do instance/ +mv transfer_stats.db instance/ +``` + +## Ověření + +Po úspěšné migraci by měl výstup vypadat takto: + +``` +============================================================ + Vytváření záloh +============================================================ +✓ Zazálohován .env → .env.backup +✓ Zazálohována databáze → instance/transfer_stats.db.backup + +============================================================ + Migrace databáze +============================================================ +✓ Nalezena databáze: instance/transfer_stats.db +✓ Vytvořena tabulka accounts +✓ Přidán sloupec account_id do tabulky stats + +Nalezen existující účet: vlastik.novotny2005@gmail.com +Zadejte přezdívku pro tento účet: Hlavní účet + +✓ Migrován účet: vlastik.novotny2005@gmail.com + ✓ Přezdívka: Hlavní účet + ✓ Aktualizováno X statistických záznamů + +✓ Migrace databáze dokončena! +``` + +## Nová verze používá instance/ automaticky + +Opravená verze `main_enhanced.py`: +```python +# Config nyní používá: +SQLALCHEMY_DATABASE_URI = "sqlite:///instance/transfer_stats.db" + +# A automaticky vytvoří instance/ složku, pokud neexistuje +``` + +Takže po migraci vše funguje správně s databází v `instance/` složce. + +## Stáhni opravené soubory + +Všechny soubory v `/mnt/user-data/outputs/` jsou již opravené a ready to use! + +```bash +# Zkopíruj opravené soubory +cd ~/urio +cp /cesta/k/stazenym/migrate.py . +cp /cesta/k/stazenym/main_enhanced.py . + +# A spusť migraci znovu +python3 migrate.py +``` + +--- + +Tohle by mělo problém vyřešit! Zkus to a dej vědět, jestli to funguje. 🚀 diff --git a/IMMEDIATE_FIX.md b/IMMEDIATE_FIX.md new file mode 100644 index 0000000..6c4e1da --- /dev/null +++ b/IMMEDIATE_FIX.md @@ -0,0 +1,129 @@ +# 🚑 OKAMŽITÁ OPRAVA - Database Path Error + +## Problém +``` +sqlite3.OperationalError: unable to open database file +``` + +## ⚡ Rychlé Řešení (30 sekund) + +### Varianta 1: Automatická oprava +```bash +cd ~/urio +python3 fix_db_path.py +python3 main.py +``` + +### Varianta 2: Manuální oprava +```bash +cd ~/urio + +# Přidej do main.py hned za class Config: +# Najdi tuto řádku (řádek ~33): +# SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///instance/transfer_stats.db") +# +# A nahraď ji tímto (3 řádky): +# BASE_DIR = os.path.abspath(os.path.dirname(__file__)) +# INSTANCE_DIR = os.path.join(BASE_DIR, 'instance') +# SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", f"sqlite:///{os.path.join(INSTANCE_DIR, 'transfer_stats.db')}") +``` + +### Varianta 3: Použij opravený soubor +```bash +cd ~/urio + +# Stáhni znovu main_enhanced.py z outputs/ +# A přepiš jím současný main.py +mv main.py main.py.broken +cp /path/to/main_enhanced.py main.py + +python3 main.py +``` + +## 🔍 Co je problém? + +SQLAlchemy používá relativní cestu `sqlite:///instance/transfer_stats.db`, ale instance folder neexistuje v době, kdy se SQLAlchemy inicializuje. + +## ✅ Co oprava dělá? + +1. **Použije absolutní cestu** místo relativní +2. **Vytvoří instance/ folder** PŘED inicializací SQLAlchemy +3. **Nastaví správnou cestu** k databázi + +## 📝 Změny v kódu + +### PŘED (nefungující): +```python +class Config: + SQLALCHEMY_DATABASE_URI = "sqlite:///instance/transfer_stats.db" + +app = Flask(__name__) +app.config.from_object(Config) +# instance folder se vytváří tady ← TOO LATE! +``` + +### PO (fungující): +```python +class Config: + BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + INSTANCE_DIR = os.path.join(BASE_DIR, 'instance') + SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(INSTANCE_DIR, 'transfer_stats.db')}" + +# instance folder se vytváří tady ← BEFORE app init! +if not os.path.exists(Config.INSTANCE_DIR): + os.makedirs(Config.INSTANCE_DIR) + +app = Flask(__name__) +app.config.from_object(Config) +``` + +## 🎯 Ověření + +Po opravě by měl výstup vypadat takto: +```bash +$ python3 main.py +2025-11-21 12:20:51,319 - INFO - Successfully loaded world map GeoJSON data. +2025-11-21 12:20:51,450 - INFO - Scheduler started + * Serving Flask app 'main' + * Running on http://0.0.0.0:90 +``` + +✅ Bez chyby "unable to open database file"! + +## 🆘 Stále nefunguje? + +Zkontroluj: + +```bash +# 1. Existuje instance složka? +ls -la instance/ + +# 2. Jsou v ní správné soubory? +ls -la instance/transfer_stats.db* + +# 3. Má Python práva na zápis? +touch instance/test.txt && rm instance/test.txt + +# 4. Je databáze validní? +sqlite3 instance/transfer_stats.db ".tables" +``` + +Pokud všechno OK, měl by výstup být: +``` +accounts settings stats webhook +``` + +--- + +## 📦 Aktualizované soubory + +Všechny soubory v `/mnt/user-data/outputs/` jsou již opravené: +- ✅ **main_enhanced.py** - Opravená verze +- ✅ **fix_db_path.py** - Automatický fix skript +- ✅ **migrate.py** - Funguje s instance/ + +Stačí je použít a vše bude fungovat! + +--- + +**Vyřešilo to problém? Dej mi vědět!** 🚀 diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..a6da15d --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,194 @@ +# 🚀 UrNetwork Stats Dashboard v2.0 - QUICK START + +## 📦 Balíček obsahuje: + +``` +urnetwork-stats-v2/ +├── main_enhanced.py # Hlavní aplikace (přejmenovat na main.py) +├── migrate.py # Migrační skript pro upgrade +├── install.sh # Instalační skript pro nové instalace +├── README_CZ.md # Kompletní dokumentace +├── UPGRADE_GUIDE.md # Průvodce upgradem +└── QUICKSTART.md # Tento soubor +``` + +## ⚡ Pro NOVOU instalaci (prázdná složka) + +```bash +# 1. Nahrajte všechny soubory do složky +cd ~/urnetwork-stats + +# 2. Přejmenujte hlavní soubor +mv main_enhanced.py main.py + +# 3. Spusťte instalační skript +bash install.sh + +# 4. Spusťte aplikaci +python3 main.py + +# 5. Otevřete v prohlížeči +http://your-server:90 +``` + +## 🔄 Pro UPGRADE existující instalace + +```bash +# 1. Zazálohujte současnou složku +cd ~ +cp -r urio urio_backup + +# 2. Nahrajte POUZE tyto soubory do existující složky: +# - main_enhanced.py → přejmenovat na main.py +# - migrate.py + +# 3. Zastavte běžící aplikaci +pkill -f main.py + +# 4. Spusťte migraci +cd ~/urio +python3 migrate.py + +# 5. Spusťte novou verzi +python3 main.py + +# 6. Přihlaste se novým admin heslem +http://your-server:90 +``` + +## 🎯 První kroky po instalaci + +### 1. Nastavení Admin Hesla +- Při první návštěvě nastavíte admin heslo +- **DŮLEŽITÉ:** Toto není vaše UrNetwork heslo! +- Toto heslo používáte pro přihlášení do dashboardu + +### 2. Přidání UrNetwork Účtů +``` +Dashboard → Správa účtů → Přidat účet + +Vyplňte: + ✓ UrNetwork email (vlastik.novotny2005@gmail.com) + ✓ UrNetwork heslo + ✓ Přezdívka (např. "Domácí účet") +``` + +### 3. Zobrazení Dat +- **Veřejný pohled**: Kombinované statistiky všech účtů +- **Privátní dashboard**: Detailní grafy a správa +- **Správa účtů**: Přidávání/odebírání účtů + +## 🆘 Rychlá pomoc + +### Nemohu se přihlásit +```bash +# Zkontrolujte .env soubor +cat .env | grep ADMIN_PASSWORD + +# Pokud chybí, přidejte +echo "ADMIN_PASSWORD=your_password" >> .env +``` + +### Aplikace neběží +```bash +# Zkontrolujte logy +tail -f nohup.out + +# Nebo spusťte v popředí pro debugging +python3 main.py +``` + +### Port 90 je obsazený +```python +# V main.py změňte poslední řádek: +app.run(host="0.0.0.0", port=8080, debug=False) +# ^^^^ +# Změňte číslo portu +``` + +### Upgrade selhal +```bash +# Obnovte zálohu +cd ~ +rm -rf urio +mv urio_backup urio +cd urio +python3 main.py +``` + +## 📚 Detailní dokumentace + +- **README_CZ.md** - Kompletní feature list a použití +- **UPGRADE_GUIDE.md** - Detailní průvodce upgradem +- **GitHub Issues** - Pro reportování problémů + +## 🎨 Screenshots + +### Veřejný Dashboard +``` +┌─────────────────────────────────────────┐ +│ Celkem placených dat │ 45.234 GB │ +│ Celkem neplacených dat │ 12.456 GB │ +│ Aktivní účty │ 3 │ +│ Výdělky (30 dní) │ $125.67 │ +└─────────────────────────────────────────┘ + +Graf: Kombinovaná data ze všech účtů +Graf: Domácí účet +Graf: Pracovní účet +Graf: Server účet + +Mapa: Lokace poskytovatelů +``` + +### Správa Účtů +``` +┌──────────────────────────────────────────────┐ +│ Přezdívka │ Username │ Stav │ +├──────────────────────────────────────────────┤ +│ [Domácí účet] │ vlastik@... │ Aktivní │ +│ [Pracovní] │ work@... │ Aktivní │ +│ [Server] │ server@... │ Neaktivní│ +└──────────────────────────────────────────────┘ +``` + +## 🔥 Hlavní Nové Funkce + +✅ **Multi-Account** - Sledujte více UrNetwork účtů +✅ **Glassmorphism Design** - Moderní UI s blur efekty +✅ **Kombinované Stats** - Agregovaná data všech účtů +✅ **Individuální Grafy** - Graf pro každý účet zvlášť +✅ **Barevné Odlišení** - Každý účet má svou barvu +✅ **Admin Heslo** - Oddělené od UrNetwork credentials +✅ **Toggle Účtů** - Zapínání/vypínání sledování +✅ **Přezdívky** - Pojmenujte si účty jak chcete + +## 💡 Tipy + +### Pro více než 3 účty +- Snižte frekvenci fetchování v kódu (z 15 na 30 minut) +- Používejte přezdívky pro lepší přehled + +### Pro produkční nasazení +- Nastavte `FORCE_HTTPS=True` v .env +- Použijte Gunicorn místo development serveru +- Nastavte systemd service pro autostart + +### Webhooky +```json +{ + "content": "📊 **${account}**\n💾 Data: ${total_gb} GB\n🕐 ${update_time}" +} +``` + +## 📞 Kontakt & Podpora + +- Original project: techroy23/UrNetwork-Stats-Dashboard +- Enhanced by: Claude/Anthropic +- Issues & Questions: GitHub Issues + +--- + +**Enjoy! 🎉** + +Pro detaily viz README_CZ.md diff --git a/README_CZ.md b/README_CZ.md new file mode 100644 index 0000000..6aef3ce --- /dev/null +++ b/README_CZ.md @@ -0,0 +1,172 @@ +# UrNetwork Stats Dashboard - Vylepšená Multi-Account Verze v2.1 + +🇨🇿 **Český README** | [🇬🇧 English README](README.md) + +--- + +## 🌟 Rychlé Odkazy + +- 🚀 [Rychlý Start](QUICKSTART.md) - Spusť za 5 minut +- 📊 [Návod na Webhooky](WEBHOOK_GUIDE.md) - Discord, Slack, Telegram příklady +- 🎨 [Porovnání Designů](DESIGN_COMPARISON.md) - Proč tento design? +- 🔄 [Průvodce Upgradem](UPGRADE_GUIDE.md) - Migrace z v1.0 +- 📝 [Changelog](CHANGELOG.md) - Historie verzí + +--- + +## O Projektu + +Toto je **výrazně vylepšená multi-account verze** původního [UrNetwork Stats Dashboard](https://github.com/techroy23/UrNetwork-Stats-Dashboard). Sleduj neomezený počet UrNetwork účtů s krásnými grafy, real-time aktualizacemi a webhook notifikacemi. + +**Co je nového ve v2.1:** +- 🎯 **Multi-Account Podpora** - Sleduj neomezený počet UrNetwork účtů +- 🎨 **Čistý Design** - Cloudflare-inspirovaný tmavý vzhled +- 📊 **Lepší Grafy** - Bílý text, tooltip kdekoliv na grafu +- 🔐 **Oddělená Auth** - Admin heslo nezávislé na UrNetwork +- ⚡ **Denní Čištění** - Auto-mazání dat starších 7 dní +- 📱 **Mobilní Optimalizace** - Touch-friendly rozhraní + +> **Vylepšeno AI:** v2.0+ vytvořeno s Claude (Anthropic) pro lepší UX, multi-account podporu a moderní design. + +--- + +## ✨ Klíčové Funkce + +### 🎯 Multi-Account Správa +- ✅ Sleduj neomezený počet UrNetwork účtů současně +- ✅ Kombinované statistiky ze všech účtů +- ✅ Individuální grafy pro každý účet +- ✅ Vlastní přezdívky pro snadnou identifikaci +- ✅ Zapni/vypni účty bez smazání +- ✅ Barevně odlišené pro snadné rozpoznání + +### 📊 Pokročilá Vizualizace +- ✅ Interaktivní Chart.js grafy s tooltipem **kdekoliv na grafu** +- ✅ Celková data, Placená vs Neplacená, Delta grafy +- ✅ Bílý čitelný text na všech grafech (konec mrákání!) +- ✅ Mapa světa ukazující lokace poskytovatelů +- ✅ Real-time aktualizace každou minutu + +### ⚙️ Automatizace +- ✅ Sběr dat každých 15 minut +- ✅ Denní čištění (ponechává posledních 7 dní) +- ✅ Webhook notifikace (Discord, Slack, Telegram) +- ✅ Správa zařízení napříč všemi účty + +### 🎨 Moderní UI +- ✅ Čistý Cloudflare-inspirovaný tmavý vzhled +- ✅ Bez gradientů (lepší viditelnost grafů) +- ✅ Modrá akcent barva (#3b82f6) +- ✅ Plně responzivní (mobil, tablet, desktop) +- ✅ Podpora češtiny a angličtiny + +--- + +## 🚀 Rychlý Start + +```bash +# 1. Klonovat repozitář +git clone https://forgejo.plainrock127.xyz/mxnticek/UrNetwork-Stats-Dashboard-remade +cd UrNetwork-Stats-Dashboard-remade + +# 2. Instalovat závislosti +pip install Flask Flask-SQLAlchemy Flask-APScheduler requests python-dateutil + +# 3. Spustit +python main.py + +# 4. Otevřít prohlížeč +# http://localhost:90 +``` + +**První Spuštění:** +1. Nastav své **admin heslo** (NENÍ to tvé UrNetwork heslo!) +2. Přidej své UrNetwork účty (email + heslo + přezdívka) +3. Hotovo! Dashboard začne sbírat data každých 15 minut + +📖 **Potřebuješ více detailů?** Viz [QUICKSTART.md](QUICKSTART.md) + +--- + +## 📊 Nastavení Webhooků - Rychlé Příklady + +### Discord - Jednoduchý +```json +{ + "content": "📊 **${account}**: ${total_gb} GB" +} +``` + +### Discord - Bohatý Embed (Doporučeno) +```json +{ + "username": "UrNetwork Bot", + "embeds": [{ + "title": "⚡ ${account} - Nová Data!", + "color": 3901635, + "fields": [ + {"name": "💰 Placená", "value": "`${paid_gb} GB`", "inline": true}, + {"name": "📡 Neplacená", "value": "`${unpaid_gb} GB`", "inline": true}, + {"name": "💾 Celkem", "value": "**${total_gb} GB**", "inline": false} + ], + "footer": {"text": "UrNetwork Stats"}, + "timestamp": "${update_time}" + }] +} +``` + +### Telegram +```json +{ + "chat_id": "TVOJE_CHAT_ID", + "text": "📊 *${account}*\n💾 Celkem: ${total_gb} GB\n💰 Placená: ${paid_gb} GB\n📡 Neplacená: ${unpaid_gb} GB", + "parse_mode": "Markdown" +} +``` + +**Dostupné Proměnné:** +- `${account}` - Přezdívka účtu +- `${paid_gb}` - Placená data v GB +- `${unpaid_gb}` - Neplacená data v GB +- `${total_gb}` - Celková data v GB +- `${update_time}` - Časová značka + +📖 **Kompletní webhook návod:** [WEBHOOK_GUIDE.md](WEBHOOK_GUIDE.md) + +--- + +## 🎨 Proč Tento Design? + +Po rozsáhlém testování jsme zvolili **čistý Cloudflare-inspirovaný design** protože: + +1. **Lepší Viditelnost Grafů** - Pevné tmavé pozadí = lepší kontrast +2. **Profesionální Vzhled** - Napodobuje enterprise dashboardy +3. **Bez Únav Očí** - Čisté barvy bez gradientů +4. **Výkon** - Žádné blur efekty = rychlejší rendering +5. **Přístupnost** - Vysoké kontrastní poměry + +**Barevné Schéma:** +```css +Pozadí: #0c0d0e (Tmavá) +Primární: #3b82f6 (Modrá) +Text: #e5e7eb (Světlá) +Úspěch: #10b981 (Zelená) +Chyba: #ef4444 (Červená) +``` + +📖 **Detaily designu:** [DESIGN_COMPARISON.md](DESIGN_COMPARISON.md) + +--- + +## 🙏 Poděkování + +- **Originál:** [techroy23/UrNetwork-Stats-Dashboard](https://github.com/techroy23/UrNetwork-Stats-Dashboard) +- **v2.0+ Vylepšeno:** Claude (Anthropic AI) +- **Design Inspirace:** Cloudflare, Vercel +- **Speciální Díky:** Vlastík (mxnticek) za testování + +--- + +**Verze:** v2.1.1 | **Poslední Aktualizace:** 21. listopadu 2024 + +**Vytvořeno s ❤️ a 🤖 AI** diff --git a/UPGRADE_GUIDE.md b/UPGRADE_GUIDE.md new file mode 100644 index 0000000..0296898 --- /dev/null +++ b/UPGRADE_GUIDE.md @@ -0,0 +1,304 @@ +# UrNetwork Stats Dashboard v2.0 - Upgrade Guide + +## 🎯 Hlavní Vylepšení + +### 1. Multi-Account Support ⭐ NOVÉ +**Před:** +- Sledování pouze jednoho UrNetwork účtu +- Nutnost manuálně měnit credentials v .env +- Žádná kombinovaná statistika + +**Po:** +``` +✓ Neomezený počet UrNetwork účtů +✓ Kombinovaná statistika ze všech účtů +✓ Individuální grafy pro každý účet +✓ Snadné přepínání mezi účty +✓ Přezdívky pro lepší orientaci +✓ Zapínání/vypínání sledování +``` + +### 2. Redesign UI 🎨 VYLEPŠENO + +**Design změny:** +```css +Před: Flat dark theme +Po: Glassmorphism + Gradients + +Barvy: + Před: Modrá (#3b82f6) + Po: Fialová gradient (#667eea → #764ba2) + +Efekty: + ✓ Blur backdrop filters + ✓ Smooth hover animations + ✓ Shadow depth effects + ✓ Gradient overlays + ✓ Animated transitions +``` + +### 3. Lepší Bezpečnost 🔐 VYLEPŠENO + +**Před:** +``` +- Přihlášení pomocí UrNetwork credentials +- Hesla v .env otevřeně +``` + +**Po:** +``` +✓ Oddělené admin heslo pro dashboard +✓ UrNetwork credentials bezpečně v databázi +✓ Session management +✓ HTTPS redirect podpora +``` + +### 4. Vylepšené Grafy 📊 ROZŠÍŘENO + +**Nové možnosti:** +- Kombinovaný graf (všechny účty dohromady) +- Individuální grafy pro každý účet +- Barevné odlišení účtů +- Přepínání mezi pohledy +- Lepší legenda a tooltips + +### 5. Správa Zařízení 📱 ROZŠÍŘENO + +**Před:** +``` +Device Name | Status | Client ID | Mode | Remove +``` + +**Po:** +``` +Account | Device Name | Status | Client ID | Mode | Remove + ↓ +Označení které zařízení patří kterému účtu +``` + +## 📊 Srovnání Funkcí + +| Funkce | v1.0 | v2.0 | +|--------|------|------| +| Počet účtů | 1 | Neomezeno | +| Design | Basic Dark | Glassmorphism | +| Grafy | Základní | Multi-account + Combined | +| Admin panel | UrNetwork login | Oddělené admin heslo | +| Webhooky | Základní | S account proměnnou | +| Správa účtů | Manuálně v .env | GUI správa | +| Toggle účtů | ❌ | ✅ | +| Přezdívky | ❌ | ✅ | +| Barevné odlišení | ❌ | ✅ | +| Kombinované stats | ❌ | ✅ | + +## 🚀 Upgrade Proces + +### Krok 1: Příprava +```bash +# 1. Zastavte běžící aplikaci +pkill -f main.py + +# 2. Přejděte do složky aplikace +cd ~/urio + +# 3. Stáhněte nové soubory +# (nebo je nahrajte manuálně) +``` + +### Krok 2: Migrace +```bash +# Spusťte migrační skript +python3 migrate.py +``` + +**Migrační skript automaticky:** +1. ✓ Vytvoří zálohy (.env.backup, transfer_stats.db.backup) +2. ✓ Aktualizuje strukturu databáze +3. ✓ Přidá tabulku accounts +4. ✓ Migruje existující účet +5. ✓ Nastaví admin heslo +6. ✓ Ověří migraci + +### Krok 3: Test +```bash +# Spusťte novou verzi +python3 main.py + +# Otevřete v prohlížeči +http://your-server:90 + +# Přihlaste se pomocím NOVÉHO admin hesla +``` + +### Krok 4: Přidání účtů +``` +1. Přihlaste se do dashboardu +2. Menu → Správa účtů +3. Klikněte "Přidat účet" +4. Vyplňte: + - UrNetwork email + - UrNetwork heslo + - Přezdívku (např. "Domácí", "Práce") +5. Účet se automaticky ověří +``` + +## 🔄 Rollback (pokud něco nejde) + +### Obnovení zálohy +```bash +# Zastavte aplikaci +pkill -f main.py + +# Obnovte zálohy +cp .env.backup .env +cp transfer_stats.db.backup transfer_stats.db +mv main.py.old main.py + +# Spusťte starou verzi +python3 main.py +``` + +## 📱 Použití Multi-Account + +### Přidání druhého účtu +``` +Příklad: Máte 2 UrNetwork účty + +Účet 1 (Existující): + Email: vlastik.novotny2005@gmail.com + → Automaticky migrován + → Přezdívka: "Hlavní účet" + +Účet 2 (Nový): + Email: druhy.ucet@gmail.com + Heslo: ******** + → Přidán přes GUI + → Přezdívka: "Pracovní účet" +``` + +### Zobrazení kombinovaných dat +``` +Veřejný dashboard: + → Zobrazí součet dat obou účtů + → Individual grafy pro každý účet + → Celkové výdělky ze všech účtů + +Privátní dashboard: + → Toggle: "Kombinovaná statistika" / "Jednotlivé účty" + → Filtr podle účtu v sekci Account & Leaderboard + → Zařízení označená účtem +``` + +## 🎨 CSS Customizace + +### Změna barevného schématu +V `main_enhanced.py`, změňte CSS proměnné: + +```css +:root { + /* Změňte tyto hodnoty pro vlastní barevné schéma */ + --primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --success: #10b981; + --danger: #ef4444; +} +``` + +### Populární barevné schémata: + +**Modrá (Ocean):** +```css +--primary: linear-gradient(135deg, #667eea 0%, #00d4ff 100%); +``` + +**Zelená (Nature):** +```css +--primary: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); +``` + +**Oranžová (Sunset):** +```css +--primary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); +``` + +## 🐛 Časté Problémy + +### Problém: "Nelze se přihlásit" +**Řešení:** +```bash +# Zkontrolujte ADMIN_PASSWORD v .env +cat .env | grep ADMIN_PASSWORD + +# Pokud chybí, přidejte ho +echo "ADMIN_PASSWORD=your_password" >> .env +``` + +### Problém: "Účet se nepřidává" +**Řešení:** +1. Zkontrolujte UrNetwork credentials +2. Zkuste se přihlásit na bringyour.com +3. Zkontrolujte logy: `tail -f nohup.out` + +### Problém: "Chybí data po migraci" +**Řešení:** +```bash +# Ověřte account_id ve stats tabulce +sqlite3 transfer_stats.db "SELECT COUNT(*) FROM stats WHERE account_id IS NULL;" + +# Pokud je > 0, přiřaďte manuálně +sqlite3 transfer_stats.db "UPDATE stats SET account_id = 1 WHERE account_id IS NULL;" +``` + +### Problém: "Webhook nefunguje" +**Řešení:** +``` +Starý formát: + {"content": "Data: ${total_gb} GB"} + +Nový formát: + {"content": "Account: ${account}, Data: ${total_gb} GB"} + ^^^^^^^^^^^ + Nová proměnná +``` + +## 📈 Performance Tips + +### Pro více než 5 účtů: +```python +# V main_enhanced.py, upravte job interval +@scheduler.task(id="log_stats_job", trigger="cron", minute="0,30") +# místo: minute="0,15,30,45" +# Tím snížíte frekvenci API callů +``` + +### Pro lepší responzivitu: +```bash +# Použijte Gunicorn místo development serveru +pip install gunicorn +gunicorn -w 4 -b 0.0.0.0:90 main_enhanced:app +``` + +## 🎯 Co Dělat Dál + +1. **Přidejte další účty** + - Menu → Správa účtů → Přidat účet + +2. **Nastavte webhooky** + - Menu → Nastavení → Webhook Management + - Použijte `${account}` pro rozlišení účtů + +3. **Customizujte design** + - Změňte CSS proměnné podle svých preferencí + +4. **Monitoring** + - Zkontrolujte logy pravidelně + - Nastavte alerting přes webhooky + +## 📞 Podpora + +- **GitHub Issues**: Pro bug reporty a feature requesty +- **Documentation**: README_CZ.md pro detailní dokumentaci +- **Migration Help**: migrate.py --help pro nápovědu + +--- + +**Enjoy your new multi-account dashboard! 🚀** diff --git a/WEBHOOK_GUIDE.md b/WEBHOOK_GUIDE.md new file mode 100644 index 0000000..2eda464 --- /dev/null +++ b/WEBHOOK_GUIDE.md @@ -0,0 +1,391 @@ +# 🔔 Webhook Průvodce - UrNetwork Stats Dashboard + +## 📋 Dostupné Proměnné + +Když webhook systém posílá notifikace, máš k dispozici tyto proměnné: + +| Proměnná | Popis | Příklad | Formát | +|----------|-------|---------|--------| +| `${account}` | Přezdívka účtu | "Hlavní účet" | Text | +| `${paid_gb}` | Placená data v GB | "123.456" | 3 des. místa | +| `${unpaid_gb}` | Neplacená data v GB | "45.678" | 3 des. místa | +| `${total_gb}` | Celková data v GB | "169.134" | 3 des. místa | +| `${update_time}` | Čas aktualizace | "2024-11-21 12:30:45" | YYYY-MM-DD HH:MM:SS | + +## 🎯 Kdy Se Webhooky Spouštějí? + +- **Každých 15 minut** - Když scheduler načte nová data +- **Pro každý aktivní účet** zvlášť +- Pouze pokud se načtení dat povede + +--- + +## 🔥 Příklady Pro Tebe (Vlastík) + +### 1. Discord - Jednoduchá Notifikace 💬 + +```json +{ + "content": "📊 **${account}** aktualizace!\n💾 Celkem: **${total_gb} GB**\n💰 Placená: ${paid_gb} GB | 📡 Neplacená: ${unpaid_gb} GB\n🕐 ${update_time}" +} +``` + +**Výstup:** +``` +📊 **Hlavní účet** aktualizace! +💾 Celkem: **169.134 GB** +💰 Placená: 123.456 GB | 📡 Neplacená: 45.678 GB +🕐 2024-11-21 12:30:45 +``` + +--- + +### 2. Discord - Bohatý Embed (Český) 🇨🇿 + +```json +{ + "embeds": [{ + "title": "📊 UrNetwork - ${account}", + "description": "Nová data byla načtena!", + "color": 3901635, + "fields": [ + { + "name": "💰 Placená Data", + "value": "${paid_gb} GB", + "inline": true + }, + { + "name": "📡 Neplacená Data", + "value": "${unpaid_gb} GB", + "inline": true + }, + { + "name": "💾 Celkem Poskytnutých Dat", + "value": "**${total_gb} GB**", + "inline": false + } + ], + "footer": { + "text": "Aktualizováno" + }, + "timestamp": "${update_time}" + }] +} +``` + +--- + +### 3. Discord - Minimalistický 🎯 + +```json +{ + "content": "⚡ ${account}: **${total_gb} GB** (${paid_gb} / ${unpaid_gb})" +} +``` + +**Výstup:** +``` +⚡ Hlavní účet: **169.134 GB** (123.456 / 45.678) +``` + +--- + +### 4. Discord - S Progress Barem 📊 + +```json +{ + "embeds": [{ + "title": "⚡ ${account} Stats", + "color": 3447003, + "description": "**Celkem: ${total_gb} GB**\n\n💰 Placená: `${paid_gb} GB`\n📡 Neplacená: `${unpaid_gb} GB`", + "footer": { + "text": "${update_time}" + } + }] +} +``` + +--- + +### 5. Slack - Jednoduchá Zpráva 💼 + +```json +{ + "text": ":bar_chart: *${account}* - Aktualizace\nCelkem: *${total_gb} GB* | Placená: ${paid_gb} GB | Neplacená: ${unpaid_gb} GB\n_${update_time}_" +} +``` + +--- + +### 6. Slack - Formátovaná Zpráva 💼 + +```json +{ + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "📊 UrNetwork Stats - ${account}" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Placená Data:*\n${paid_gb} GB" + }, + { + "type": "mrkdwn", + "text": "*Neplacená Data:*\n${unpaid_gb} GB" + }, + { + "type": "mrkdwn", + "text": "*Celkem:*\n${total_gb} GB" + }, + { + "type": "mrkdwn", + "text": "*Čas:*\n${update_time}" + } + ] + } + ] +} +``` + +--- + +### 7. Telegram - Markdown 📱 + +```json +{ + "chat_id": "YOUR_CHAT_ID", + "text": "📊 *${account}* - Aktualizace\n\n💾 *Celkem:* ${total_gb} GB\n💰 Placená: ${paid_gb} GB\n📡 Neplacená: ${unpaid_gb} GB\n\n🕐 _${update_time}_", + "parse_mode": "Markdown" +} +``` + +--- + +### 8. MS Teams - Jednoduchá Karta 🏢 + +```json +{ + "@type": "MessageCard", + "@context": "https://schema.org/extensions", + "summary": "${account} Stats Update", + "themeColor": "0078D7", + "title": "📊 ${account} - UrNetwork Stats", + "sections": [{ + "facts": [ + { + "name": "Placená Data:", + "value": "${paid_gb} GB" + }, + { + "name": "Neplacená Data:", + "value": "${unpaid_gb} GB" + }, + { + "name": "Celkem:", + "value": "${total_gb} GB" + } + ], + "text": "Aktualizováno: ${update_time}" + }] +} +``` + +--- + +### 9. Generic JSON (pro vlastní API) 🔧 + +```json +{ + "event": "urnetwork_update", + "account_name": "${account}", + "data": { + "paid_gb": "${paid_gb}", + "unpaid_gb": "${unpaid_gb}", + "total_gb": "${total_gb}" + }, + "timestamp": "${update_time}" +} +``` + +--- + +### 10. IFTTT Webhook 🔗 + +```json +{ + "value1": "${account}", + "value2": "${total_gb} GB", + "value3": "Placená: ${paid_gb} GB, Neplacená: ${unpaid_gb} GB" +} +``` + +--- + +## 🎨 Doporučené Pro Tebe + +Protože máš UrNetwork setup a pravděpodobně používáš Discord/Telegram, doporučuji: + +### ⭐ Nejlepší Volba - Discord Embed (Český) + +```json +{ + "username": "UrNetwork Bot", + "avatar_url": "https://cdn-icons-png.flaticon.com/512/2920/2920277.png", + "embeds": [{ + "title": "⚡ ${account} - Nová Data!", + "color": 3901635, + "fields": [ + { + "name": "💰 Placená Data", + "value": "`${paid_gb} GB`", + "inline": true + }, + { + "name": "📡 Neplacená Data", + "value": "`${unpaid_gb} GB`", + "inline": true + }, + { + "name": "💾 Celkem", + "value": "**${total_gb} GB**", + "inline": false + } + ], + "footer": { + "text": "UrNetwork Stats Dashboard" + }, + "timestamp": "${update_time}" + }] +} +``` + +### 🚀 Rychlá Varianta - Pro Mobil + +```json +{ + "content": "⚡ **${account}**: ${total_gb} GB\n💰 ${paid_gb} | 📡 ${unpaid_gb}" +} +``` + +--- + +## 📝 Jak Přidat Webhook + +### 1. Discord Webhook URL +``` +1. Jdi do Discord serveru +2. Server Settings → Integrations → Webhooks +3. New Webhook +4. Zkopíruj Webhook URL +5. Vlož do dashboardu v Settings → Webhook Management +``` + +### 2. V Dashboardu +``` +1. Přihlaš se do dashboardu +2. Menu → Settings (Nastavení) +3. Webhook Management +4. Vlož URL: https://discord.com/api/webhooks/... +5. Vlož JSON payload (výše) +6. Přidat Webhook +``` + +--- + +## 🧪 Test Webhooku + +Chceš otestovat, jestli webhook funguje? Spusť manuální fetch: + +```bash +# V dashboardu klikni "Fetch Now" +# nebo z příkazové řádky: +curl -X POST http://localhost:90/trigger +``` + +--- + +## 🔍 Debug + +### Pokud webhook nefunguje: + +1. **Zkontroluj logy:** +```bash +tail -f nohup.out | grep webhook +``` + +2. **Zkontroluj JSON syntax:** +```bash +# Použij online JSON validator +# https://jsonlint.com/ +``` + +3. **Test Discord webhooku:** +```bash +curl -X POST "YOUR_WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d '{"content": "Test message"}' +``` + +--- + +## 💡 Pro Tipy + +### Více Webhooků +Můžeš přidat více webhooků - každý dostane notifikaci! +- Discord server 1 +- Discord server 2 +- Slack workspace +- Telegram bot + +### Filtrování Podle Účtu +Chceš notifikace jen pro specifický účet? + +V payloadu použij podmínku (pokud tvůj webhook systém podporuje): +```json +{ + "content": "${account} update: ${total_gb} GB" +} +``` + +A vytvoř samostatný webhook jen když je `${account}` = "Hlavní účet" + +### Rate Limiting +Pozor! Většina služeb má rate limit: +- Discord: 5 zpráv / 2 sekundy per webhook +- Slack: 1 zpráva / sekunda + +Dashboard posílá notifikace každých 15 minut, takže jsi v pohodě! ✅ + +--- + +## 🎯 Tvoje Setup (Doporučení) + +Protože máš pravděpodobně několik účtů, doporučuji: + +```json +{ + "embeds": [{ + "title": "📊 ${account}", + "description": "**${total_gb} GB** poskytnutých dat", + "color": 3901635, + "fields": [ + {"name": "💰 Placená", "value": "${paid_gb} GB", "inline": true}, + {"name": "📡 Neplacená", "value": "${unpaid_gb} GB", "inline": true} + ], + "footer": {"text": "${update_time}"} + }] +} +``` + +Takto dostaneš **jednu zprávu každých 15 minut pro každý účet** s přehledným rozdělením! 🎉 + +--- + +**Potřebuješ pomoct s nastavením? Dej vědět! 🚀** diff --git a/fix_db_path.py b/fix_db_path.py new file mode 100644 index 0000000..80f2c13 --- /dev/null +++ b/fix_db_path.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +Quick fix script for database path issue +Fixes the main.py file to use absolute paths +""" + +import os +import sys + +def fix_main_py(): + """Fix main.py to use absolute database path""" + + main_file = 'main.py' + + if not os.path.exists(main_file): + print("❌ main.py not found in current directory") + print(" Run this script from the urio folder") + sys.exit(1) + + print("🔧 Fixing database path in main.py...") + + # Read the file + with open(main_file, 'r') as f: + content = f.read() + + # Check if already fixed + if 'BASE_DIR = os.path.abspath(os.path.dirname(__file__))' in content: + print("✅ main.py is already fixed!") + return + + # Fix 1: Update Config class + old_config = '''class Config: + """Flask configuration.""" + SCHEDULER_API_ENABLED = True + SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///instance/transfer_stats.db")''' + + new_config = '''class Config: + """Flask configuration.""" + SCHEDULER_API_ENABLED = True + # Use absolute path for database + BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + INSTANCE_DIR = os.path.join(BASE_DIR, 'instance') + SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", f"sqlite:///{os.path.join(INSTANCE_DIR, 'transfer_stats.db')}")''' + + if old_config in content: + content = content.replace(old_config, new_config) + print("✅ Updated Config class with absolute paths") + + # Fix 2: Ensure instance folder creation happens before app init + old_init = '''# --- Flask App Initialization --- + +app = Flask(__name__) +app.config.from_object(Config) + +# Ensure instance folder exists +instance_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'instance') +if not os.path.exists(instance_path): + os.makedirs(instance_path)''' + + new_init = '''# --- Flask App Initialization --- + +# Ensure instance folder exists BEFORE creating the app +instance_path = Config.INSTANCE_DIR +if not os.path.exists(instance_path): + os.makedirs(instance_path) + logging.info(f"Created instance directory: {instance_path}") + +app = Flask(__name__) +app.config.from_object(Config)''' + + if old_init in content: + content = content.replace(old_init, new_init) + print("✅ Updated instance folder creation") + + # Write the fixed content + with open(main_file, 'w') as f: + f.write(content) + + print("\n✅ main.py has been fixed!") + print("\nYou can now run:") + print(" python3 main.py") + +if __name__ == '__main__': + print(""" +╔════════════════════════════════════════════════════════════╗ +║ UrNetwork Stats Dashboard - Database Path Fix ║ +╚════════════════════════════════════════════════════════════╝ + """) + + try: + fix_main_py() + except Exception as e: + print(f"\n❌ Error: {e}") + sys.exit(1) diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..61650eb --- /dev/null +++ b/install.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +# UrNetwork Stats Dashboard - Installation Script v2.1 +# This script automates the installation process + +set -e # Exit on error + +echo "=========================================" +echo "UrNetwork Stats Dashboard v2.1" +echo "Installation Script" +echo "=========================================" +echo "" + +# Check Python version +echo "[1/6] Checking Python version..." +if ! command -v python3 &> /dev/null; then + echo "❌ Python 3 is not installed!" + echo "Please install Python 3.8+ and try again." + exit 1 +fi + +PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))') +echo "✅ Python $PYTHON_VERSION found" +echo "" + +# Check pip +echo "[2/6] Checking pip..." +if ! command -v pip3 &> /dev/null; then + echo "❌ pip3 is not installed!" + echo "Please install pip3 and try again." + exit 1 +fi +echo "✅ pip3 found" +echo "" + +# Install dependencies +echo "[3/6] Installing dependencies..." +echo "This may take a few minutes..." +pip3 install -r requirements.txt +echo "✅ Dependencies installed" +echo "" + +# Create necessary directories +echo "[4/6] Creating directories..." +mkdir -p instance +echo "✅ Directories created" +echo "" + +# Check if already installed +echo "[5/6] Checking existing installation..." +if [ -f ".env" ]; then + echo "⚠️ Found existing .env file" + read -p "Do you want to keep it? (y/n): " keep_env + if [ "$keep_env" != "y" ]; then + mv .env .env.backup + echo "✅ Backed up to .env.backup" + else + echo "✅ Keeping existing configuration" + fi +fi + +if [ -f "instance/transfer_stats.db" ]; then + echo "⚠️ Found existing database" + read -p "Do you want to keep it? (y/n): " keep_db + if [ "$keep_db" != "y" ]; then + mv instance/transfer_stats.db instance/transfer_stats.db.backup + echo "✅ Backed up to transfer_stats.db.backup" + else + echo "✅ Keeping existing database" + fi +fi +echo "" + +# Final instructions +echo "[6/6] Installation complete!" +echo "" +echo "=========================================" +echo "🎉 Ready to start!" +echo "=========================================" +echo "" +echo "To run the dashboard:" +echo " python3 main.py" +echo "" +echo "Then open your browser to:" +echo " http://localhost:90" +echo "" +echo "On first run, you will:" +echo " 1. Set your admin password" +echo " 2. Add your UrNetwork accounts" +echo "" +echo "For production deployment with Gunicorn:" +echo " gunicorn --bind 0.0.0.0:90 --workers 4 main:app" +echo "" +echo "For systemd service setup, see README.md" +echo "" +echo "📖 Documentation:" +echo " - README.md - Full documentation" +echo " - QUICKSTART.md - Quick start guide" +echo " - WEBHOOK_GUIDE.md - Webhook examples" +echo "" +echo "Need help? Check the docs or create an issue!" +echo "=========================================" diff --git a/main_clean.py b/main_clean.py new file mode 100644 index 0000000..e2de183 --- /dev/null +++ b/main_clean.py @@ -0,0 +1,2221 @@ +# ---------------------------------------- +# UrNetwork Stats Dashboard - Enhanced Multi-Account Edition +# ---------------------------------------- +# Enhanced with modern design, multi-account support, and combined statistics +# ---------------------------------------- + +import os +import time +import datetime +import requests +import logging +import json +from functools import wraps +from flask import ( + Flask, request, render_template_string, + redirect, url_for, flash, session, g, jsonify +) +from flask_sqlalchemy import SQLAlchemy +from flask_apscheduler import APScheduler +from dateutil.parser import isoparse +import secrets +from string import Template + +# --- Application Setup & Configuration --- + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +ENV_FILE = ".env" + +def load_env(): + """Load environment variables from .env file.""" + if os.path.exists(ENV_FILE): + with open(ENV_FILE, 'r') as f: + for line in f: + if '=' in line and not line.strip().startswith('#'): + key, value = line.strip().split('=', 1) + os.environ[key] = value + +load_env() + +class Config: + """Flask configuration.""" + SCHEDULER_API_ENABLED = True + # Use absolute path for database + BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + INSTANCE_DIR = os.path.join(BASE_DIR, 'instance') + SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", f"sqlite:///{os.path.join(INSTANCE_DIR, 'transfer_stats.db')}") + SQLALCHEMY_TRACK_MODIFICATIONS = False + SECRET_KEY = os.getenv("SECRET_KEY", "default-secret-key-for-initial-setup") + UR_API_BASE = "https://api.bringyour.com" + FORCE_HTTPS = os.getenv("FORCE_HTTPS", "False").lower() in ('true', '1', 't') + ENABLE_ACCOUNT_STATS = os.getenv("ENABLE_ACCOUNT_STATS", "True").lower() in ('true', '1', 't') + ENABLE_LEADERBOARD = os.getenv("ENABLE_LEADERBOARD", "True").lower() in ('true', '1', 't') + ENABLE_DEVICE_STATS = os.getenv("ENABLE_DEVICE_STATS", "True").lower() in ('true', '1', 't') + +# --- Flask App Initialization --- + +# Ensure instance folder exists BEFORE creating the app +instance_path = Config.INSTANCE_DIR +if not os.path.exists(instance_path): + os.makedirs(instance_path) + logging.info(f"Created instance directory: {instance_path}") + +app = Flask(__name__) +app.config.from_object(Config) + +db = SQLAlchemy(app) +scheduler = APScheduler() +scheduler.init_app(app) + +# --- Database Models --- + +class Account(db.Model): + """Represents a UrNetwork account.""" + __tablename__ = 'accounts' + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(100), unique=True, nullable=False) + password = db.Column(db.String(200), nullable=False) + nickname = db.Column(db.String(100), nullable=True) + is_active = db.Column(db.Boolean, default=True) + created_at = db.Column(db.DateTime, server_default=db.func.now()) + +class Stats(db.Model): + """Represents a snapshot of paid vs unpaid bytes at a given timestamp.""" + __tablename__ = 'stats' + id = db.Column(db.Integer, primary_key=True) + account_id = db.Column(db.Integer, db.ForeignKey('accounts.id'), nullable=False) + timestamp = db.Column(db.DateTime, server_default=db.func.now()) + paid_bytes = db.Column(db.BigInteger, nullable=False) + paid_gb = db.Column(db.Float, nullable=False) + unpaid_bytes = db.Column(db.BigInteger, nullable=False) + unpaid_gb = db.Column(db.Float, nullable=False) + account = db.relationship('Account', backref='stats') + +class Webhook(db.Model): + """Represents a webhook URL with an optional custom payload.""" + __tablename__ = 'webhook' + id = db.Column(db.Integer, primary_key=True) + url = db.Column(db.String, unique=True, nullable=False) + payload = db.Column(db.Text, nullable=True) + +class Setting(db.Model): + """Represents a key-value setting for the application.""" + __tablename__ = 'settings' + key = db.Column(db.String(50), primary_key=True) + value = db.Column(db.String(100), nullable=False) + +# --- Helper Functions --- + +def get_setting(key, default=None): + """Gets a setting value from the database.""" + setting = Setting.query.get(key) + return setting.value if setting else default + +def get_boolean_setting(key): + """Gets a boolean setting from the database.""" + value = get_setting(key, 'False') + return value.lower() in ('true', '1', 't') + +def is_installed(): + """Check if the application has been configured.""" + return os.getenv("SECRET_KEY") and os.getenv("SECRET_KEY") != "default-secret-key-for-initial-setup" + +def save_env_file(config_data): + """Save configuration data to the .env file.""" + try: + existing_env = {} + if os.path.exists(ENV_FILE): + with open(ENV_FILE, 'r') as f: + for line in f: + if '=' in line and not line.strip().startswith('#'): + key, value = line.strip().split('=', 1) + existing_env[key] = value + + existing_env.update(config_data) + + with open(ENV_FILE, "w") as f: + for key, value in existing_env.items(): + f.write(f"{key}={value}\n") + if "ENABLE_ACCOUNT_STATS" not in existing_env: + f.write("\n# Feature Flags\n") + f.write("ENABLE_ACCOUNT_STATS=True\n") + f.write("ENABLE_LEADERBOARD=True\n") + f.write("ENABLE_DEVICE_STATS=True\n") + if "FORCE_HTTPS" not in existing_env: + f.write("\n# Security Settings\n") + f.write("FORCE_HTTPS=False\n") + load_env() + return True + except IOError as e: + logging.error(f"Failed to write to .env file: {e}") + return False + +def request_with_retry(method, url, retries=3, backoff=5, timeout=30, **kwargs): + """Issue an HTTP request with retries.""" + last_exc = None + for attempt in range(1, retries + 1): + try: + resp = requests.request(method, url, timeout=timeout, **kwargs) + resp.raise_for_status() + return resp + except requests.exceptions.RequestException as e: + last_exc = e + logging.warning(f"[{method.upper()} {url}] attempt {attempt}/{retries} failed: {e}") + if attempt < retries: + time.sleep(backoff) + logging.error(f"All {retries} attempts to {method.upper()} {url} failed: {last_exc}") + return None + +def get_jwt_from_credentials(user, password): + """Fetch a new JWT token using username and password.""" + try: + resp = request_with_retry( + "post", + f"{app.config['UR_API_BASE']}/auth/login-with-password", + headers={"Content-Type": "application/json"}, + json={"user_auth": user, "password": password}, + ) + if not resp: + raise RuntimeError("API request failed after multiple retries.") + data = resp.json() + token = data.get("network", {}).get("by_jwt") + if not token: + err = data.get("message") or data.get("error") or str(data) + raise RuntimeError(f"Login failed: {err}") + return token + except Exception as e: + logging.error(f"Could not get JWT from credentials: {e}") + return None + +def get_valid_jwt(account): + """Gets a valid JWT for API calls for a specific account.""" + if not account: + return None + return get_jwt_from_credentials(account.username, account.password) + +# --- API Fetch Functions --- + +def fetch_transfer_stats(jwt_token): + """Retrieve transfer statistics using the provided JWT.""" + if not jwt_token: return None + resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/transfer/stats", headers={"Authorization": f"Bearer {jwt_token}"}) + if not resp: return None + data = resp.json() + paid = data.get("paid_bytes_provided", 0) + unpaid = data.get("unpaid_bytes_provided", 0) + return { + "paid_bytes": paid, + "paid_gb": paid / 1e9, + "unpaid_bytes": unpaid, + "unpaid_gb": unpaid / 1e9 + } + +def fetch_payment_stats(jwt_token): + """Retrieve account payment statistics using the provided JWT.""" + if not jwt_token: return [] + resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/account/payments", headers={"Authorization": f"Bearer {jwt_token}"}) + if not resp: return [] + return resp.json().get("account_payments", []) + +def fetch_account_details(jwt_token): + """Fetches various account details like points and referrals.""" + if not jwt_token: return {} + headers = {"Authorization": f"Bearer {jwt_token}"} + details = {} + + points_resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/account/points", headers=headers) + if points_resp: + points_data = points_resp.json().get("network_points", []) + details['points'] = sum(p.get('point_value', 0) for p in points_data) + + referral_resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/account/referral-code", headers=headers) + if referral_resp: + details['referrals'] = referral_resp.json() + + ranking_resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/network/ranking", headers=headers) + if ranking_resp: + details['ranking'] = ranking_resp.json().get('network_ranking', {}) + + return details + +def fetch_leaderboard(jwt_token): + """Fetches the global leaderboard.""" + if not jwt_token: return [] + headers = {"Authorization": f"Bearer {jwt_token}"} + resp = request_with_retry("post", f"{app.config['UR_API_BASE']}/stats/leaderboard", headers=headers, json={}) + if not resp: return [] + return resp.json().get("earners", []) + +def fetch_devices(jwt_token): + """Fetches the status of all network clients/devices.""" + if not jwt_token: return [] + headers = {"Authorization": f"Bearer {jwt_token}"} + resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/network/clients", headers=headers) + if not resp: return [] + devices = resp.json().get("clients", []) + provide_mode_map = {-1: "Default", 0: "None", 1: "Network", 2: "Friends & Family", 3: "Public", 4: "Stream"} + for device in devices: + device['provide_mode_str'] = provide_mode_map.get(device.get('provide_mode'), 'Unknown') + return devices + +def remove_device(jwt_token, client_id): + """Removes a device from the network.""" + if not jwt_token: return False, "Authentication token not available." + headers = {"Authorization": f"Bearer {jwt_token}"} + payload = {"client_id": client_id} + resp = request_with_retry("post", f"{app.config['UR_API_BASE']}/network/remove-client", headers=headers, json=payload) + if resp and resp.status_code == 200: + data = resp.json() + if data.get("error"): + return False, data["error"].get("message", "An unknown error occurred.") + return True, "Device removed successfully." + elif resp: + return False, f"API returned status {resp.status_code}." + else: + return False, "Failed to communicate with API." + +def calculate_earnings(payments): + """Calculate total and monthly earnings from a list of payments.""" + total_earnings = 0 + monthly_earnings = 0 + now = datetime.datetime.now(datetime.timezone.utc) + one_month_ago = now - datetime.timedelta(days=30) + + if not payments: + return 0, 0 + + for payment in payments: + if payment.get("completed"): + amount = payment.get("token_amount", 0) + total_earnings += amount + payment_time_str = payment.get("payment_time") + if payment_time_str: + try: + payment_time = isoparse(payment_time_str) + if payment_time > one_month_ago: + monthly_earnings += amount + except (ValueError, TypeError): + logging.warning(f"Could not parse payment_time: {payment_time_str}") + + return total_earnings, monthly_earnings + +def fetch_provider_locations(): + """Retrieve provider locations from the public API.""" + resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/network/provider-locations") + return resp.json() if resp else None + +def send_webhook_notification(stats_data, account_nickname=None): + """Sends a notification to all configured webhooks.""" + with app.app_context(): + webhooks = Webhook.query.all() + if not webhooks: + return + + for webhook in webhooks: + payload_to_send = None + try: + if webhook.payload: + template = Template(webhook.payload) + payload_str = template.safe_substitute( + account=account_nickname or "Unknown", + paid_gb=f"{stats_data['paid_gb']:.3f}", + unpaid_gb=f"{stats_data['unpaid_gb']:.3f}", + total_gb=f"{(stats_data['paid_gb'] + stats_data['unpaid_gb']):.3f}", + update_time=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ) + payload_to_send = json.loads(payload_str) + else: + payload_to_send = { + "embeds": [{ + "title": f"UrNetwork Stats Update - {account_nickname or 'Account'}", + "description": "New data has been synced from the UrNetwork API.", + "color": 5814783, + "fields": [ + {"name": "Account", "value": account_nickname or "Unknown", "inline": False}, + {"name": "Total Paid Data", "value": f"{stats_data['paid_gb']:.3f} GB", "inline": True}, + {"name": "Total Unpaid Data", "value": f"{stats_data['unpaid_gb']:.3f} GB", "inline": True}, + {"name": "Total Data Provided", "value": f"{(stats_data['paid_gb'] + stats_data['unpaid_gb']):.3f} GB", "inline": True}, + ], + "footer": {"text": f"Update Time: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"} + }] + } + + requests.post(webhook.url, json=payload_to_send, timeout=10) + logging.info(f"Sent webhook notification to {webhook.url}") + except (json.JSONDecodeError, TypeError) as e: + logging.error(f"Failed to parse or substitute custom payload for webhook {webhook.url}: {e}") + except requests.exceptions.RequestException as e: + logging.error(f"Failed to send webhook to {webhook.url}: {e}") + +# --- Authentication Decorator --- +def login_required(f): + """Decorator to ensure a user is logged in.""" + @wraps(f) + def decorated_function(*args, **kwargs): + if not session.get('logged_in'): + flash(g.t['error_login_required'], "error") + next_url = request.url + if app.config['FORCE_HTTPS']: + next_url = next_url.replace('http://', 'https://', 1) + return redirect(url_for('login', next=next_url)) + return f(*args, **kwargs) + return decorated_function + +# --- Scheduled Jobs --- + +@scheduler.task(id="log_stats_job", trigger="cron", minute="0,15,30,45") +def log_stats_job(): + """Scheduled job to fetch and store stats every 15 minutes for all active accounts.""" + with app.app_context(): + if not is_installed(): + logging.warning("log_stats_job skipped: Application is not installed.") + return + + logging.info("Running scheduled stats fetch for all accounts...") + accounts = Account.query.filter_by(is_active=True).all() + + for account in accounts: + try: + jwt = get_valid_jwt(account) + if not jwt: + logging.warning(f"Could not authenticate account {account.username}") + continue + + stats_data = fetch_transfer_stats(jwt) + if not stats_data: + logging.warning(f"Could not fetch stats for account {account.username}") + continue + + entry = Stats( + account_id=account.id, + paid_bytes=stats_data["paid_bytes"], + paid_gb=stats_data["paid_gb"], + unpaid_bytes=stats_data["unpaid_bytes"], + unpaid_gb=stats_data["unpaid_gb"] + ) + db.session.add(entry) + db.session.commit() + logging.info(f"Logged stats for account {account.nickname or account.username} at {entry.timestamp}") + send_webhook_notification(stats_data, account.nickname or account.username) + except Exception as e: + logging.error(f"Failed to fetch stats for account {account.username}: {e}") + +@scheduler.task(id="cleanup_old_stats_job", trigger="cron", hour="3", minute="0") +def cleanup_old_stats_job(): + """Scheduled job to delete stats data older than 7 days, runs daily at 3 AM.""" + with app.app_context(): + if not is_installed(): + logging.warning("cleanup_old_stats_job skipped: Application is not installed.") + return + + logging.info("Running daily stats cleanup job...") + try: + cutoff_date = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=7) + num_rows_deleted = db.session.query(Stats).filter(Stats.timestamp < cutoff_date).delete(synchronize_session=False) + db.session.commit() + + if num_rows_deleted > 0: + logging.info(f"Successfully deleted {num_rows_deleted} stats records older than 7 days.") + else: + logging.info("No old stats records found to delete.") + except Exception as e: + logging.error(f"Scheduled job 'cleanup_old_stats_job' failed: {e}") + db.session.rollback() + +# --- Internationalization (i18n) --- + +TEXTS = { + 'cs': { + 'nav_public': 'Veřejný pohled', + 'nav_owner_dashboard': 'Panel vlastníka', + 'nav_overview': 'Přehled', + 'nav_account': 'Účet & Žebříček', + 'nav_devices': 'Zařízení', + 'nav_settings': 'Nastavení', + 'nav_accounts': 'Správa účtů', + 'nav_login': 'Přihlásit se', + 'nav_logout': 'Odhlásit se', + 'title_public_dashboard': 'Veřejný panel', + 'title_owner_dashboard': 'Panel vlastníka', + 'title_account_page': 'Účet & Žebříček', + 'title_devices_page': 'Správa zařízení', + 'title_settings': 'Nastavení', + 'title_accounts_page': 'Správa účtů', + 'title_login': 'Přihlásit se', + 'title_setup': 'Počáteční nastavení', + 'card_paid_data': 'Celkem placených dat', + 'card_unpaid_data': 'Celkem neplacených dat', + 'card_earnings_30d': 'Výdělky (30 dní)', + 'card_total_earnings': 'Celkové výdělky', + 'card_last_update': 'Poslední aktualizace', + 'card_account_points': 'Body účtu', + 'card_your_rank': 'Vaše pozice', + 'card_total_referrals': 'Celkem doporučení', + 'card_active_accounts': 'Aktivní účty', + 'card_total_devices': 'Celkem zařízení', + 'chart_total_data_gb': 'Celková poskytnutá data (GB)', + 'chart_paid_vs_unpaid': 'Placená vs. neplacená data (GB)', + 'chart_delta': 'Změna dat za interval (GB)', + 'chart_paid': 'Placená (GB)', + 'chart_unpaid': 'Neplacená (GB)', + 'chart_no_data': 'Nedostatek dat pro zobrazení grafu.', + 'history_title': 'Detailní historie', + 'history_timestamp': 'Časová značka', + 'history_change': 'Změna (GB)', + 'btn_fetch_now': 'Načíst nyní', + 'btn_clear_data': 'Vymazat všechna data', + 'btn_add_account': 'Přidat účet', + 'confirm_clear_data': 'Jste si jisti? Tímto smažete všechna historická data statistik.', + 'next_fetch': 'Další automatické načtení:', + 'login_header': 'Přihlaste se do svého panelu', + 'login_username': 'Administrátorské heslo', + 'login_password': 'Heslo', + 'login_button': 'Přihlásit se', + 'error_login_required': 'Pro zobrazení této stránky se musíte přihlásit.', + 'error_invalid_credentials': 'Neplatné heslo.', + 'flash_logged_in': 'Byli jste úspěšně přihlášeni.', + 'flash_logged_out': 'Byli jste odhlášeni.', + 'flash_settings_saved': 'Nastavení bylo úspěšně uloženo.', + 'flash_fetch_success': 'Nejnovější statistiky byly úspěšně načteny a uloženy.', + 'flash_fetch_fail': 'Nepodařilo se načíst statistiky: ', + 'flash_clear_success': 'Úspěšně smazáno {count} záznamů z databáze.', + 'flash_clear_fail': 'Při mazání databáze došlo k chybě.', + 'flash_account_added': 'Účet byl úspěšně přidán.', + 'flash_account_removed': 'Účet byl úspěšně odstraněn.', + 'flash_account_updated': 'Účet byl úspěšně aktualizován.', + 'webhook_title': 'Správa webhooků', + 'webhook_desc': 'Přidejte webhooky pro zasílání oznámení.', + 'webhook_url_label': 'URL adresa webhooku', + 'webhook_payload_label': 'Vlastní JSON Payload (volitelné)', + 'webhook_payload_placeholder': 'Např.: {"content": "Účet: ${account}, Data: ${total_gb} GB"}', + 'webhook_add_btn': 'Přidat Webhook', + 'webhook_current': 'Aktuální Webhooky', + 'webhook_delete_btn': 'Smazat', + 'webhook_flash_added': 'Webhook úspěšně přidán.', + 'webhook_flash_deleted': 'Webhook úspěšně smazán.', + 'webhook_flash_invalid_url': 'Je vyžadována platná URL adresa webhooku.', + 'webhook_flash_exists': 'Tato URL adresa webhooku je již zaregistrována.', + 'accounts_username': 'Uživatelské jméno', + 'accounts_nickname': 'Přezdívka', + 'accounts_status': 'Stav', + 'accounts_actions': 'Akce', + 'accounts_active': 'Aktivní', + 'accounts_inactive': 'Neaktivní', + 'accounts_toggle': 'Přepnout', + 'accounts_remove': 'Odebrat', + 'accounts_add_title': 'Přidat nový účet', + 'accounts_add_username': 'UrNetwork uživatelské jméno', + 'accounts_add_password': 'UrNetwork heslo', + 'accounts_add_nickname': 'Přezdívka (volitelné)', + 'combined_stats': 'Kombinovaná statistika', + 'individual_accounts': 'Jednotlivé účty', + 'view_combined': 'Zobrazit kombinovanou statistiku', + 'view_individual': 'Zobrazit jednotlivé účty', + 'map_title': 'Lokace poskytovatelů', + 'map_legend_title': 'Počet poskytovatelů', + 'map_legend_hover': 'Přejeďte myší přes zemi', + 'leaderboard_title': 'Žebříček', + 'leaderboard_rank': 'Pořadí', + 'leaderboard_name': 'Jméno sítě', + 'leaderboard_data': 'Poskytnutá data (MiB)', + 'devices_title': 'Stav a správa zařízení', + 'devices_status': 'Stav', + 'devices_name': 'Jméno zařízení', + 'devices_id': 'ID klienta', + 'devices_mode': 'Režim poskytování', + 'devices_account': 'Účet', + 'devices_remove': 'Odebrat', + 'devices_online': 'Online', + 'devices_offline': 'Offline', + 'devices_confirm_remove': 'Opravdu chcete odebrat toto zařízení?', + 'flash_device_removed': 'Zařízení úspěšně odebráno.', + 'flash_device_remove_fail': 'Nepodařilo se odebrat zařízení: ' + }, + 'en': { + 'nav_public': 'Public View', + 'nav_owner_dashboard': 'Owner Dashboard', + 'nav_overview': 'Overview', + 'nav_account': 'Account & Leaderboard', + 'nav_devices': 'Devices', + 'nav_settings': 'Settings', + 'nav_accounts': 'Account Management', + 'nav_login': 'Login', + 'nav_logout': 'Logout', + 'title_public_dashboard': 'Public Dashboard', + 'title_owner_dashboard': 'Owner Dashboard', + 'title_account_page': 'Account & Leaderboard', + 'title_devices_page': 'Device Management', + 'title_settings': 'Settings', + 'title_accounts_page': 'Account Management', + 'title_login': 'Login', + 'title_setup': 'Initial Setup', + 'card_paid_data': 'Total Paid Data', + 'card_unpaid_data': 'Total Unpaid Data', + 'card_earnings_30d': 'Earnings (30 Days)', + 'card_total_earnings': 'Total Earnings', + 'card_last_update': 'Last Update', + 'card_account_points': 'Account Points', + 'card_your_rank': 'Your Rank', + 'card_total_referrals': 'Total Referrals', + 'card_active_accounts': 'Active Accounts', + 'card_total_devices': 'Total Devices', + 'chart_total_data_gb': 'Total Data Provided (GB)', + 'chart_paid_vs_unpaid': 'Paid vs. Unpaid Data (GB)', + 'chart_delta': 'Data Change per Interval (GB)', + 'chart_paid': 'Paid (GB)', + 'chart_unpaid': 'Unpaid (GB)', + 'chart_no_data': 'Not enough data to display a chart.', + 'history_title': 'Detailed History', + 'history_timestamp': 'Timestamp', + 'history_change': 'Change (GB)', + 'btn_fetch_now': 'Fetch Now', + 'btn_clear_data': 'Clear All Data', + 'btn_add_account': 'Add Account', + 'confirm_clear_data': 'Are you sure? This will delete all historical stats data.', + 'next_fetch': 'Next automatic fetch:', + 'login_header': 'Sign in to your dashboard', + 'login_username': 'Admin Password', + 'login_password': 'Password', + 'login_button': 'Sign in', + 'error_login_required': 'You must be logged in to view this page.', + 'error_invalid_credentials': 'Invalid password.', + 'flash_logged_in': 'You have been logged in successfully.', + 'flash_logged_out': 'You have been logged out.', + 'flash_settings_saved': 'Settings saved successfully.', + 'flash_fetch_success': 'Successfully fetched and saved latest stats.', + 'flash_fetch_fail': 'Failed to fetch stats: ', + 'flash_clear_success': 'Successfully cleared {count} records from the database.', + 'flash_clear_fail': 'An error occurred while clearing the database.', + 'flash_account_added': 'Account added successfully.', + 'flash_account_removed': 'Account removed successfully.', + 'flash_account_updated': 'Account updated successfully.', + 'webhook_title': 'Webhook Management', + 'webhook_desc': 'Add webhooks to send notifications.', + 'webhook_url_label': 'Webhook URL', + 'webhook_payload_label': 'Custom JSON Payload (optional)', + 'webhook_payload_placeholder': 'E.g.: {"content": "Account: ${account}, Data: ${total_gb} GB"}', + 'webhook_add_btn': 'Add Webhook', + 'webhook_current': 'Current Webhooks', + 'webhook_delete_btn': 'Delete', + 'webhook_flash_added': 'Webhook added successfully.', + 'webhook_flash_deleted': 'Webhook deleted successfully.', + 'webhook_flash_invalid_url': 'A valid Webhook URL is required.', + 'webhook_flash_exists': 'This webhook URL is already registered.', + 'accounts_username': 'Username', + 'accounts_nickname': 'Nickname', + 'accounts_status': 'Status', + 'accounts_actions': 'Actions', + 'accounts_active': 'Active', + 'accounts_inactive': 'Inactive', + 'accounts_toggle': 'Toggle', + 'accounts_remove': 'Remove', + 'accounts_add_title': 'Add New Account', + 'accounts_add_username': 'UrNetwork Username', + 'accounts_add_password': 'UrNetwork Password', + 'accounts_add_nickname': 'Nickname (optional)', + 'combined_stats': 'Combined Statistics', + 'individual_accounts': 'Individual Accounts', + 'view_combined': 'View Combined Stats', + 'view_individual': 'View Individual Accounts', + 'map_title': 'Provider Locations', + 'map_legend_title': 'Provider Count', + 'map_legend_hover': 'Hover over a country', + 'leaderboard_title': 'Leaderboard', + 'leaderboard_rank': 'Rank', + 'leaderboard_name': 'Network Name', + 'leaderboard_data': 'Data Provided (MiB)', + 'devices_title': 'Device Status & Management', + 'devices_status': 'Status', + 'devices_name': 'Device Name', + 'devices_id': 'Client ID', + 'devices_mode': 'Provide Mode', + 'devices_account': 'Account', + 'devices_remove': 'Remove', + 'devices_online': 'Online', + 'devices_offline': 'Offline', + 'devices_confirm_remove': 'Are you sure you want to remove this device?', + 'flash_device_removed': 'Device removed successfully.', + 'flash_device_remove_fail': 'Failed to remove device: ' + } +} + +def get_locale(): + """Detects the best language match from request headers.""" + return request.accept_languages.best_match(TEXTS.keys()) or 'en' + +# --- HTML Templates --- + +LAYOUT_TEMPLATE = """ + + + + + + {{ title }} + + + + + + + + + + +
+
+
+ + +
+
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + {{ content | safe }} +
+
+
+ + +""" + +INSTALL_TEMPLATE = """ +
+

{{ title }}

+

Nastavte administrátorské heslo pro váš dashboard.

+
+ + + +
+

Poté budete moci přidat své UrNetwork účty v nastavení.

+
+""" + +LOGIN_TEMPLATE = """ +
+

{{ title }}

+

{{ t.login_header }}

+
+ + +
+
+""" + +PUBLIC_DASHBOARD_TEMPLATE = """ +

{{ title }}

+
+
+
+
{{ t.card_paid_data }}
{{ "%.3f"|format(combined.paid_gb) }} GB
+
{{ t.card_unpaid_data }}
{{ "%.3f"|format(combined.unpaid_gb) }} GB
+
{{ t.card_active_accounts }}
{{ active_accounts }}
+
{{ t.card_earnings_30d }}
${{ "%.2f"|format(monthly_earnings) }}
+
+
+
+
+

{{ t.chart_total_data_gb }} - {{ t.combined_stats }}

+
+ {% if chart_data.labels %}{% else %}

{{ t.chart_no_data }}

{% endif %} +
+
+ + {% for account, acc_data in account_charts.items() %} +
+

{{ t.chart_total_data_gb }}

+
+ {% if acc_data.labels %}{% else %}

{{ t.chart_no_data }}

{% endif %} +
+
+ {% endfor %} + +
+

{{ t.map_title }}

+
+
+
+
+ +""" + +ACCOUNTS_MANAGE_TEMPLATE = """ +

{{ title }}

+
+
+

{{ t.accounts_add_title }}

+
+ + + + + + + + + + +
+
+ +
+

{{ t.individual_accounts }}

+ + + + + + + + + + + {% for account in accounts %} + + + + + + + {% else %} + + + + {% endfor %} + +
{{ t.accounts_nickname }}{{ t.accounts_username }}{{ t.accounts_status }}{{ t.accounts_actions }}
{{ account.username }} + + {{ t.accounts_active if account.is_active else t.accounts_inactive }} + + +
+ +
+
+ +
+
Zatím nemáte přidané žádné účty.
+
+
+""" + +PRIVATE_DASHBOARD_REACT_TEMPLATE = """ + + + + +
+ + +""" + +SETTINGS_TEMPLATE = """ +

{{ title }}

+
+
+

{{ t.webhook_title }}

+

{{ t.webhook_desc }}

+
+ + + + + +
+
+
+

{{ t.webhook_current }}

+ {% for webhook in webhooks %} +
+

{{ webhook.url }}

+
{{ webhook.payload or 'Default Discord Payload' }}
+
+ +
+
+ {% else %} +

Zatím nemáte nakonfigurované žádné webhooky.

+ {% endfor %} +
+
+""" + +# World map data +WORLD_MAP_DATA = None +try: + map_url = "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson" + response = requests.get(map_url, timeout=15) + response.raise_for_status() + WORLD_MAP_DATA = response.json() + logging.info("Successfully loaded world map GeoJSON data.") +except Exception as e: + logging.error(f"Could not download world map data: {e}") + +def render_page(template_content, **context): + """Renders a page by injecting its content into the main layout.""" + context['world_map_data'] = WORLD_MAP_DATA + content = render_template_string(template_content, t=g.t, **context) + return render_template_string(LAYOUT_TEMPLATE, content=content, t=g.t, **context) + +# --- Middleware and Before Request Handlers --- + +@app.before_request +def force_secure(): + """Redirect non-secure requests to HTTPS if FORCE_HTTPS is enabled.""" + if app.config['FORCE_HTTPS'] and not request.is_secure: + if request.headers.get('X-Forwarded-Proto', '').lower() != 'https': + secure_url = request.url.replace('http://', 'https://', 1) + return redirect(secure_url, code=301) + +@app.before_request +def check_installation_and_init(): + """Before each request, check if app is installed and set up globals.""" + g.locale = get_locale() + g.t = TEXTS[g.locale] + + if request.endpoint not in ['install', 'static'] and not is_installed(): + return redirect(url_for('install')) + + if is_installed() and request.endpoint not in ['static']: + with app.app_context(): + if not Setting.query.first(): + logging.info("First run: Initializing settings.") + settings_to_add = [ + Setting(key='ENABLE_ACCOUNT_STATS', value=str(app.config['ENABLE_ACCOUNT_STATS'])), + Setting(key='ENABLE_LEADERBOARD', value=str(app.config['ENABLE_LEADERBOARD'])), + Setting(key='ENABLE_DEVICE_STATS', value=str(app.config['ENABLE_DEVICE_STATS'])), + ] + db.session.bulk_save_objects(settings_to_add) + db.session.commit() + + g.now = datetime.datetime.utcnow() + +@app.context_processor +def inject_layout_vars(): + """Inject variables into all templates.""" + return {"now": g.now, "t": g.t} + +# --- Routes --- + +@app.route('/install', methods=['GET', 'POST']) +def install(): + if is_installed(): + flash("Aplikace je již nainstalována.", "info") + return redirect(url_for('public_dashboard')) + + if request.method == 'POST': + admin_pass = request.form.get('admin_pass') + admin_pass_confirm = request.form.get('admin_pass_confirm') + + if not admin_pass or not admin_pass_confirm: + flash("Vyplňte obě pole s heslem.", "error") + return render_page(INSTALL_TEMPLATE, title=g.t['title_setup']) + + if admin_pass != admin_pass_confirm: + flash("Hesla se neshodují.", "error") + return render_page(INSTALL_TEMPLATE, title=g.t['title_setup']) + + config_data = { + "SECRET_KEY": secrets.token_hex(24), + "ADMIN_PASSWORD": admin_pass + } + + if save_env_file(config_data): + with app.app_context(): + db.create_all() + session['logged_in'] = True + flash("Dashboard byl úspěšně nainstalován! Nyní můžete přidat své UrNetwork účty.", "info") + return redirect(url_for('accounts_manage')) + else: + flash("Chyba při ukládání konfigurace.", "error") + + return render_page(INSTALL_TEMPLATE, title=g.t['title_setup']) + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if session.get('logged_in'): + return redirect(url_for('private_dashboard')) + + if request.method == 'POST': + password = request.form.get('password') + admin_password = os.getenv('ADMIN_PASSWORD') + + if password and password == admin_password: + session['logged_in'] = True + flash(g.t['flash_logged_in'], 'info') + next_url = request.args.get('next') + return redirect(next_url or url_for('private_dashboard')) + else: + flash(g.t['error_invalid_credentials'], 'error') + + return render_page(LOGIN_TEMPLATE, title=g.t['title_login']) + +@app.route('/logout') +def logout(): + session.pop('logged_in', None) + flash(g.t['flash_logged_out'], 'info') + return redirect(url_for('public_dashboard')) + +@app.route('/') +def public_dashboard(): + """Display the public-facing dashboard with combined stats and individual account charts.""" + accounts = Account.query.filter_by(is_active=True).all() + active_accounts = len(accounts) + + # Get combined statistics + combined_paid = 0 + combined_unpaid = 0 + monthly_earnings_total = 0 + + account_charts = {} + colors = ['#667eea', '#764ba2', '#f59e0b', '#10b981', '#ef4444', '#3b82f6'] + + for idx, account in enumerate(accounts): + latest_stat = Stats.query.filter_by(account_id=account.id).order_by(Stats.timestamp.desc()).first() + if latest_stat: + combined_paid += latest_stat.paid_gb + combined_unpaid += latest_stat.unpaid_gb + + # Get JWT and payments + jwt = get_valid_jwt(account) + if jwt: + payments = fetch_payment_stats(jwt) + _, monthly = calculate_earnings(payments) + monthly_earnings_total += monthly + + # Get chart data for this account + entries = Stats.query.filter_by(account_id=account.id).order_by(Stats.timestamp.asc()).all() + if entries: + account_name = account.nickname or account.username + color = colors[idx % len(colors)] + account_charts[account_name] = { + "labels": [e.timestamp.strftime('%m-%d %H:%M') for e in entries], + "data": [e.paid_gb + e.unpaid_gb for e in entries], + "color": color, + "bgColor": f"rgba({int(color[1:3], 16)}, {int(color[3:5], 16)}, {int(color[5:7], 16)}, 0.2)" + } + + combined = { + "paid_gb": combined_paid, + "unpaid_gb": combined_unpaid + } + + # Combined chart data + all_entries = Stats.query.order_by(Stats.timestamp.asc()).all() + time_grouped = {} + for entry in all_entries: + time_key = entry.timestamp.strftime('%m-%d %H:%M') + if time_key not in time_grouped: + time_grouped[time_key] = {"paid": 0, "unpaid": 0} + time_grouped[time_key]["paid"] += entry.paid_gb + time_grouped[time_key]["unpaid"] += entry.unpaid_gb + + chart_data = { + "labels": list(time_grouped.keys()), + "data": [time_grouped[k]["paid"] + time_grouped[k]["unpaid"] for k in time_grouped.keys()] + } + + return render_page( + PUBLIC_DASHBOARD_TEMPLATE, + title=g.t['title_public_dashboard'], + combined=combined, + active_accounts=active_accounts, + monthly_earnings=monthly_earnings_total, + chart_data=chart_data, + account_charts=account_charts + ) + +@app.route('/dashboard') +@login_required +def private_dashboard(): + react_props = get_react_props('overview') + return render_page(PRIVATE_DASHBOARD_REACT_TEMPLATE, title=g.t['title_owner_dashboard'], react_props=react_props) + +def get_react_props(initial_page): + """Helper to build the props dictionary for the React app.""" + return { + "initial_page": initial_page, + "translations": g.t + } + +# --- Account Management Routes --- + +@app.route('/accounts') +@login_required +def accounts_manage(): + accounts = Account.query.all() + return render_page(ACCOUNTS_MANAGE_TEMPLATE, title=g.t['title_accounts_page'], accounts=accounts) + +@app.route('/accounts/add', methods=['POST']) +@login_required +def accounts_add(): + username = request.form.get('username') + password = request.form.get('password') + nickname = request.form.get('nickname') + + if not username or not password: + flash("Uživatelské jméno a heslo jsou povinné.", "error") + return redirect(url_for('accounts_manage')) + + # Verify credentials + jwt = get_jwt_from_credentials(username, password) + if not jwt: + flash("Nepodařilo se ověřit přihlašovací údaje. Zkontrolujte je a zkuste to znovu.", "error") + return redirect(url_for('accounts_manage')) + + # Check if account already exists + existing = Account.query.filter_by(username=username).first() + if existing: + flash("Tento účet je již přidán.", "error") + return redirect(url_for('accounts_manage')) + + account = Account(username=username, password=password, nickname=nickname) + db.session.add(account) + db.session.commit() + + flash(g.t['flash_account_added'], "info") + return redirect(url_for('accounts_manage')) + +@app.route('/accounts/toggle/', methods=['POST']) +@login_required +def accounts_toggle(account_id): + account = Account.query.get_or_404(account_id) + account.is_active = not account.is_active + db.session.commit() + flash(g.t['flash_account_updated'], "info") + return redirect(url_for('accounts_manage')) + +@app.route('/accounts/remove/', methods=['POST']) +@login_required +def accounts_remove(account_id): + account = Account.query.get_or_404(account_id) + # Delete all stats for this account + Stats.query.filter_by(account_id=account_id).delete() + db.session.delete(account) + db.session.commit() + flash(g.t['flash_account_removed'], "info") + return redirect(url_for('accounts_manage')) + +# --- API Routes for React App --- + +@app.route('/api/dashboard/overview') +@login_required +def api_overview_data(): + accounts = Account.query.filter_by(is_active=True).all() + + # Combined statistics + combined_paid = 0 + combined_unpaid = 0 + total_earnings = 0 + + account_charts = {} + colors = ['#667eea', '#764ba2', '#f59e0b', '#10b981', '#ef4444', '#3b82f6'] + + for idx, account in enumerate(accounts): + # Get latest stat + latest_stat = Stats.query.filter_by(account_id=account.id).order_by(Stats.timestamp.desc()).first() + if latest_stat: + combined_paid += latest_stat.paid_gb + combined_unpaid += latest_stat.unpaid_gb + + # Get earnings + jwt = get_valid_jwt(account) + if jwt: + payments = fetch_payment_stats(jwt) + total, _ = calculate_earnings(payments) + total_earnings += total + + # Get chart data + entries = Stats.query.filter_by(account_id=account.id).order_by(Stats.timestamp.asc()).all() + if entries: + account_name = account.nickname or account.username + color = colors[idx % len(colors)] + account_charts[account_name] = { + "labels": [e.timestamp.strftime('%m-%d %H:%M') for e in entries], + "data": [e.paid_gb + e.unpaid_gb for e in entries], + "color": color, + "bgColor": f"rgba({int(color[1:3], 16)}, {int(color[3:5], 16)}, {int(color[5:7], 16)}, 0.2)" + } + + # Combined chart + all_entries = Stats.query.order_by(Stats.timestamp.asc()).all() + time_grouped = {} + for entry in all_entries: + time_key = entry.timestamp.strftime('%m-%d %H:%M') + if time_key not in time_grouped: + time_grouped[time_key] = {"paid": 0, "unpaid": 0} + time_grouped[time_key]["paid"] += entry.paid_gb + time_grouped[time_key]["unpaid"] += entry.unpaid_gb + + combined_chart = { + "labels": list(time_grouped.keys()), + "paid_gb": [time_grouped[k]["paid"] for k in time_grouped.keys()], + "unpaid_gb": [time_grouped[k]["unpaid"] for k in time_grouped.keys()] + } + + return jsonify({ + "combined": { + "paid_gb": combined_paid, + "unpaid_gb": combined_unpaid + }, + "active_accounts": len(accounts), + "total_earnings": total_earnings, + "combined_chart": combined_chart, + "account_charts": account_charts + }) + +@app.route('/api/dashboard/account') +@login_required +def api_account_data(): + account_id = request.args.get('account_id', 'all') + + accounts = Account.query.filter_by(is_active=True).all() + account_details = {} + leaderboard = [] + + if account_id != 'all': + account = Account.query.get(int(account_id)) + if account: + jwt = get_valid_jwt(account) + if jwt: + account_details = fetch_account_details(jwt) + leaderboard = fetch_leaderboard(jwt) + + return jsonify({ + "accounts": [{"id": a.id, "username": a.username, "nickname": a.nickname} for a in accounts], + "account_details": account_details, + "leaderboard": leaderboard + }) + +@app.route('/api/dashboard/devices') +@login_required +def api_devices_data(): + accounts = Account.query.filter_by(is_active=True).all() + all_devices = [] + + for account in accounts: + jwt = get_valid_jwt(account) + if jwt: + devices = fetch_devices(jwt) + for device in devices: + device['account_id'] = account.id + device['account_nickname'] = account.nickname or account.username + all_devices.extend(devices) + + return jsonify({"devices": all_devices}) + +@app.route('/api/dashboard/devices/remove//', methods=["POST"]) +@login_required +def api_remove_device(account_id, client_id): + account = Account.query.get(account_id) + if not account: + return jsonify({"success": False, "message": "Account not found"}), 404 + + jwt = get_valid_jwt(account) + success, message = remove_device(jwt, client_id) + + if success: + return jsonify({"success": True, "message": g.t['flash_device_removed']}) + else: + return jsonify({"success": False, "message": f"{g.t['flash_device_remove_fail']}{message}"}), 500 + +# --- Other Routes --- + +@app.route('/api/locations') +def get_locations(): + """API endpoint to proxy provider locations.""" + data = fetch_provider_locations() + if data: return jsonify(data) + return jsonify({"error": "Failed to fetch location data"}), 500 + +@app.route('/settings', methods=['GET', 'POST']) +@login_required +def settings(): + webhooks = Webhook.query.all() + return render_page(SETTINGS_TEMPLATE, title=g.t['title_settings'], webhooks=webhooks) + +@app.route("/webhooks/add", methods=["POST"]) +@login_required +def add_webhook(): + """Add a new webhook.""" + url = request.form.get("webhook_url") + payload = request.form.get("payload") + if not url or not (url.startswith("http://") or url.startswith("https://")): + flash(g.t['webhook_flash_invalid_url'], "error") + elif Webhook.query.filter_by(url=url).first(): + flash(g.t['webhook_flash_exists'], "error") + else: + new_webhook = Webhook(url=url, payload=payload if payload.strip() else None) + db.session.add(new_webhook) + db.session.commit() + flash(g.t['webhook_flash_added'], "info") + return redirect(url_for('settings')) + +@app.route("/webhooks/delete/", methods=["POST"]) +@login_required +def delete_webhook(webhook_id): + """Delete a webhook.""" + webhook = Webhook.query.get(webhook_id) + if webhook: + db.session.delete(webhook) + db.session.commit() + flash(g.t['webhook_flash_deleted'], "info") + return redirect(url_for('settings')) + +# --- Main Entry Point --- +if __name__ == "__main__": + with app.app_context(): + db.create_all() + scheduler.start() + app.run(host="0.0.0.0", port=90, debug=False) diff --git a/main_enhanced.py b/main_enhanced.py new file mode 100644 index 0000000..ddb83b3 --- /dev/null +++ b/main_enhanced.py @@ -0,0 +1,2149 @@ +# ---------------------------------------- +# UrNetwork Stats Dashboard - Enhanced Multi-Account Edition +# ---------------------------------------- +# Enhanced with modern design, multi-account support, and combined statistics +# ---------------------------------------- + +import os +import time +import datetime +import requests +import logging +import json +from functools import wraps +from flask import ( + Flask, request, render_template_string, + redirect, url_for, flash, session, g, jsonify +) +from flask_sqlalchemy import SQLAlchemy +from flask_apscheduler import APScheduler +from dateutil.parser import isoparse +import secrets +from string import Template + +# --- Application Setup & Configuration --- + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +ENV_FILE = ".env" + +def load_env(): + """Load environment variables from .env file.""" + if os.path.exists(ENV_FILE): + with open(ENV_FILE, 'r') as f: + for line in f: + if '=' in line and not line.strip().startswith('#'): + key, value = line.strip().split('=', 1) + os.environ[key] = value + +load_env() + +class Config: + """Flask configuration.""" + SCHEDULER_API_ENABLED = True + # Use absolute path for database + BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + INSTANCE_DIR = os.path.join(BASE_DIR, 'instance') + SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", f"sqlite:///{os.path.join(INSTANCE_DIR, 'transfer_stats.db')}") + SQLALCHEMY_TRACK_MODIFICATIONS = False + SECRET_KEY = os.getenv("SECRET_KEY", "default-secret-key-for-initial-setup") + UR_API_BASE = "https://api.bringyour.com" + FORCE_HTTPS = os.getenv("FORCE_HTTPS", "False").lower() in ('true', '1', 't') + ENABLE_ACCOUNT_STATS = os.getenv("ENABLE_ACCOUNT_STATS", "True").lower() in ('true', '1', 't') + ENABLE_LEADERBOARD = os.getenv("ENABLE_LEADERBOARD", "True").lower() in ('true', '1', 't') + ENABLE_DEVICE_STATS = os.getenv("ENABLE_DEVICE_STATS", "True").lower() in ('true', '1', 't') + +# --- Flask App Initialization --- + +# Ensure instance folder exists BEFORE creating the app +instance_path = Config.INSTANCE_DIR +if not os.path.exists(instance_path): + os.makedirs(instance_path) + logging.info(f"Created instance directory: {instance_path}") + +app = Flask(__name__) +app.config.from_object(Config) + +db = SQLAlchemy(app) +scheduler = APScheduler() +scheduler.init_app(app) + +# --- Database Models --- + +class Account(db.Model): + """Represents a UrNetwork account.""" + __tablename__ = 'accounts' + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(100), unique=True, nullable=False) + password = db.Column(db.String(200), nullable=False) + nickname = db.Column(db.String(100), nullable=True) + is_active = db.Column(db.Boolean, default=True) + created_at = db.Column(db.DateTime, server_default=db.func.now()) + +class Stats(db.Model): + """Represents a snapshot of paid vs unpaid bytes at a given timestamp.""" + __tablename__ = 'stats' + id = db.Column(db.Integer, primary_key=True) + account_id = db.Column(db.Integer, db.ForeignKey('accounts.id'), nullable=False) + timestamp = db.Column(db.DateTime, server_default=db.func.now()) + paid_bytes = db.Column(db.BigInteger, nullable=False) + paid_gb = db.Column(db.Float, nullable=False) + unpaid_bytes = db.Column(db.BigInteger, nullable=False) + unpaid_gb = db.Column(db.Float, nullable=False) + account = db.relationship('Account', backref='stats') + +class Webhook(db.Model): + """Represents a webhook URL with an optional custom payload.""" + __tablename__ = 'webhook' + id = db.Column(db.Integer, primary_key=True) + url = db.Column(db.String, unique=True, nullable=False) + payload = db.Column(db.Text, nullable=True) + +class Setting(db.Model): + """Represents a key-value setting for the application.""" + __tablename__ = 'settings' + key = db.Column(db.String(50), primary_key=True) + value = db.Column(db.String(100), nullable=False) + +# --- Helper Functions --- + +def get_setting(key, default=None): + """Gets a setting value from the database.""" + setting = Setting.query.get(key) + return setting.value if setting else default + +def get_boolean_setting(key): + """Gets a boolean setting from the database.""" + value = get_setting(key, 'False') + return value.lower() in ('true', '1', 't') + +def is_installed(): + """Check if the application has been configured.""" + return os.getenv("SECRET_KEY") and os.getenv("SECRET_KEY") != "default-secret-key-for-initial-setup" + +def save_env_file(config_data): + """Save configuration data to the .env file.""" + try: + existing_env = {} + if os.path.exists(ENV_FILE): + with open(ENV_FILE, 'r') as f: + for line in f: + if '=' in line and not line.strip().startswith('#'): + key, value = line.strip().split('=', 1) + existing_env[key] = value + + existing_env.update(config_data) + + with open(ENV_FILE, "w") as f: + for key, value in existing_env.items(): + f.write(f"{key}={value}\n") + if "ENABLE_ACCOUNT_STATS" not in existing_env: + f.write("\n# Feature Flags\n") + f.write("ENABLE_ACCOUNT_STATS=True\n") + f.write("ENABLE_LEADERBOARD=True\n") + f.write("ENABLE_DEVICE_STATS=True\n") + if "FORCE_HTTPS" not in existing_env: + f.write("\n# Security Settings\n") + f.write("FORCE_HTTPS=False\n") + load_env() + return True + except IOError as e: + logging.error(f"Failed to write to .env file: {e}") + return False + +def request_with_retry(method, url, retries=3, backoff=5, timeout=30, **kwargs): + """Issue an HTTP request with retries.""" + last_exc = None + for attempt in range(1, retries + 1): + try: + resp = requests.request(method, url, timeout=timeout, **kwargs) + resp.raise_for_status() + return resp + except requests.exceptions.RequestException as e: + last_exc = e + logging.warning(f"[{method.upper()} {url}] attempt {attempt}/{retries} failed: {e}") + if attempt < retries: + time.sleep(backoff) + logging.error(f"All {retries} attempts to {method.upper()} {url} failed: {last_exc}") + return None + +def get_jwt_from_credentials(user, password): + """Fetch a new JWT token using username and password.""" + try: + resp = request_with_retry( + "post", + f"{app.config['UR_API_BASE']}/auth/login-with-password", + headers={"Content-Type": "application/json"}, + json={"user_auth": user, "password": password}, + ) + if not resp: + raise RuntimeError("API request failed after multiple retries.") + data = resp.json() + token = data.get("network", {}).get("by_jwt") + if not token: + err = data.get("message") or data.get("error") or str(data) + raise RuntimeError(f"Login failed: {err}") + return token + except Exception as e: + logging.error(f"Could not get JWT from credentials: {e}") + return None + +def get_valid_jwt(account): + """Gets a valid JWT for API calls for a specific account.""" + if not account: + return None + return get_jwt_from_credentials(account.username, account.password) + +# --- API Fetch Functions --- + +def fetch_transfer_stats(jwt_token): + """Retrieve transfer statistics using the provided JWT.""" + if not jwt_token: return None + resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/transfer/stats", headers={"Authorization": f"Bearer {jwt_token}"}) + if not resp: return None + data = resp.json() + paid = data.get("paid_bytes_provided", 0) + unpaid = data.get("unpaid_bytes_provided", 0) + return { + "paid_bytes": paid, + "paid_gb": paid / 1e9, + "unpaid_bytes": unpaid, + "unpaid_gb": unpaid / 1e9 + } + +def fetch_payment_stats(jwt_token): + """Retrieve account payment statistics using the provided JWT.""" + if not jwt_token: return [] + resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/account/payments", headers={"Authorization": f"Bearer {jwt_token}"}) + if not resp: return [] + return resp.json().get("account_payments", []) + +def fetch_account_details(jwt_token): + """Fetches various account details like points and referrals.""" + if not jwt_token: return {} + headers = {"Authorization": f"Bearer {jwt_token}"} + details = {} + + points_resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/account/points", headers=headers) + if points_resp: + points_data = points_resp.json().get("network_points", []) + details['points'] = sum(p.get('point_value', 0) for p in points_data) + + referral_resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/account/referral-code", headers=headers) + if referral_resp: + details['referrals'] = referral_resp.json() + + ranking_resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/network/ranking", headers=headers) + if ranking_resp: + details['ranking'] = ranking_resp.json().get('network_ranking', {}) + + return details + +def fetch_leaderboard(jwt_token): + """Fetches the global leaderboard.""" + if not jwt_token: return [] + headers = {"Authorization": f"Bearer {jwt_token}"} + resp = request_with_retry("post", f"{app.config['UR_API_BASE']}/stats/leaderboard", headers=headers, json={}) + if not resp: return [] + return resp.json().get("earners", []) + +def fetch_devices(jwt_token): + """Fetches the status of all network clients/devices.""" + if not jwt_token: return [] + headers = {"Authorization": f"Bearer {jwt_token}"} + resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/network/clients", headers=headers) + if not resp: return [] + devices = resp.json().get("clients", []) + provide_mode_map = {-1: "Default", 0: "None", 1: "Network", 2: "Friends & Family", 3: "Public", 4: "Stream"} + for device in devices: + device['provide_mode_str'] = provide_mode_map.get(device.get('provide_mode'), 'Unknown') + return devices + +def remove_device(jwt_token, client_id): + """Removes a device from the network.""" + if not jwt_token: return False, "Authentication token not available." + headers = {"Authorization": f"Bearer {jwt_token}"} + payload = {"client_id": client_id} + resp = request_with_retry("post", f"{app.config['UR_API_BASE']}/network/remove-client", headers=headers, json=payload) + if resp and resp.status_code == 200: + data = resp.json() + if data.get("error"): + return False, data["error"].get("message", "An unknown error occurred.") + return True, "Device removed successfully." + elif resp: + return False, f"API returned status {resp.status_code}." + else: + return False, "Failed to communicate with API." + +def calculate_earnings(payments): + """Calculate total and monthly earnings from a list of payments.""" + total_earnings = 0 + monthly_earnings = 0 + now = datetime.datetime.now(datetime.timezone.utc) + one_month_ago = now - datetime.timedelta(days=30) + + if not payments: + return 0, 0 + + for payment in payments: + if payment.get("completed"): + amount = payment.get("token_amount", 0) + total_earnings += amount + payment_time_str = payment.get("payment_time") + if payment_time_str: + try: + payment_time = isoparse(payment_time_str) + if payment_time > one_month_ago: + monthly_earnings += amount + except (ValueError, TypeError): + logging.warning(f"Could not parse payment_time: {payment_time_str}") + + return total_earnings, monthly_earnings + +def fetch_provider_locations(): + """Retrieve provider locations from the public API.""" + resp = request_with_retry("get", f"{app.config['UR_API_BASE']}/network/provider-locations") + return resp.json() if resp else None + +def send_webhook_notification(stats_data, account_nickname=None): + """Sends a notification to all configured webhooks.""" + with app.app_context(): + webhooks = Webhook.query.all() + if not webhooks: + return + + for webhook in webhooks: + payload_to_send = None + try: + if webhook.payload: + template = Template(webhook.payload) + payload_str = template.safe_substitute( + account=account_nickname or "Unknown", + paid_gb=f"{stats_data['paid_gb']:.3f}", + unpaid_gb=f"{stats_data['unpaid_gb']:.3f}", + total_gb=f"{(stats_data['paid_gb'] + stats_data['unpaid_gb']):.3f}", + update_time=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ) + payload_to_send = json.loads(payload_str) + else: + payload_to_send = { + "embeds": [{ + "title": f"UrNetwork Stats Update - {account_nickname or 'Account'}", + "description": "New data has been synced from the UrNetwork API.", + "color": 5814783, + "fields": [ + {"name": "Account", "value": account_nickname or "Unknown", "inline": False}, + {"name": "Total Paid Data", "value": f"{stats_data['paid_gb']:.3f} GB", "inline": True}, + {"name": "Total Unpaid Data", "value": f"{stats_data['unpaid_gb']:.3f} GB", "inline": True}, + {"name": "Total Data Provided", "value": f"{(stats_data['paid_gb'] + stats_data['unpaid_gb']):.3f} GB", "inline": True}, + ], + "footer": {"text": f"Update Time: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"} + }] + } + + requests.post(webhook.url, json=payload_to_send, timeout=10) + logging.info(f"Sent webhook notification to {webhook.url}") + except (json.JSONDecodeError, TypeError) as e: + logging.error(f"Failed to parse or substitute custom payload for webhook {webhook.url}: {e}") + except requests.exceptions.RequestException as e: + logging.error(f"Failed to send webhook to {webhook.url}: {e}") + +# --- Authentication Decorator --- +def login_required(f): + """Decorator to ensure a user is logged in.""" + @wraps(f) + def decorated_function(*args, **kwargs): + if not session.get('logged_in'): + flash(g.t['error_login_required'], "error") + next_url = request.url + if app.config['FORCE_HTTPS']: + next_url = next_url.replace('http://', 'https://', 1) + return redirect(url_for('login', next=next_url)) + return f(*args, **kwargs) + return decorated_function + +# --- Scheduled Jobs --- + +@scheduler.task(id="log_stats_job", trigger="cron", minute="0,15,30,45") +def log_stats_job(): + """Scheduled job to fetch and store stats every 15 minutes for all active accounts.""" + with app.app_context(): + if not is_installed(): + logging.warning("log_stats_job skipped: Application is not installed.") + return + + logging.info("Running scheduled stats fetch for all accounts...") + accounts = Account.query.filter_by(is_active=True).all() + + for account in accounts: + try: + jwt = get_valid_jwt(account) + if not jwt: + logging.warning(f"Could not authenticate account {account.username}") + continue + + stats_data = fetch_transfer_stats(jwt) + if not stats_data: + logging.warning(f"Could not fetch stats for account {account.username}") + continue + + entry = Stats( + account_id=account.id, + paid_bytes=stats_data["paid_bytes"], + paid_gb=stats_data["paid_gb"], + unpaid_bytes=stats_data["unpaid_bytes"], + unpaid_gb=stats_data["unpaid_gb"] + ) + db.session.add(entry) + db.session.commit() + logging.info(f"Logged stats for account {account.nickname or account.username} at {entry.timestamp}") + send_webhook_notification(stats_data, account.nickname or account.username) + except Exception as e: + logging.error(f"Failed to fetch stats for account {account.username}: {e}") + +@scheduler.task(id="cleanup_old_stats_job", trigger="cron", day_of_week="sun", hour="0") +def cleanup_old_stats_job(): + """Scheduled job to delete stats data older than 7 days.""" + with app.app_context(): + if not is_installed(): + logging.warning("cleanup_old_stats_job skipped: Application is not installed.") + return + + logging.info("Running weekly stats cleanup job...") + try: + cutoff_date = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=7) + num_rows_deleted = db.session.query(Stats).filter(Stats.timestamp < cutoff_date).delete(synchronize_session=False) + db.session.commit() + + if num_rows_deleted > 0: + logging.info(f"Successfully deleted {num_rows_deleted} stats records older than 7 days.") + else: + logging.info("No old stats records found to delete.") + except Exception as e: + logging.error(f"Scheduled job 'cleanup_old_stats_job' failed: {e}") + db.session.rollback() + +# --- Internationalization (i18n) --- + +TEXTS = { + 'cs': { + 'nav_public': 'Veřejný pohled', + 'nav_owner_dashboard': 'Panel vlastníka', + 'nav_overview': 'Přehled', + 'nav_account': 'Účet & Žebříček', + 'nav_devices': 'Zařízení', + 'nav_settings': 'Nastavení', + 'nav_accounts': 'Správa účtů', + 'nav_logout': 'Odhlásit se', + 'title_public_dashboard': 'Veřejný panel', + 'title_owner_dashboard': 'Panel vlastníka', + 'title_account_page': 'Účet & Žebříček', + 'title_devices_page': 'Správa zařízení', + 'title_settings': 'Nastavení', + 'title_accounts_page': 'Správa účtů', + 'title_login': 'Přihlásit se', + 'title_setup': 'Počáteční nastavení', + 'card_paid_data': 'Celkem placených dat', + 'card_unpaid_data': 'Celkem neplacených dat', + 'card_earnings_30d': 'Výdělky (30 dní)', + 'card_total_earnings': 'Celkové výdělky', + 'card_last_update': 'Poslední aktualizace', + 'card_account_points': 'Body účtu', + 'card_your_rank': 'Vaše pozice', + 'card_total_referrals': 'Celkem doporučení', + 'card_active_accounts': 'Aktivní účty', + 'card_total_devices': 'Celkem zařízení', + 'chart_total_data_gb': 'Celková poskytnutá data (GB)', + 'chart_paid_vs_unpaid': 'Placená vs. neplacená data (GB)', + 'chart_delta': 'Změna dat za interval (GB)', + 'chart_paid': 'Placená (GB)', + 'chart_unpaid': 'Neplacená (GB)', + 'chart_no_data': 'Nedostatek dat pro zobrazení grafu.', + 'history_title': 'Detailní historie', + 'history_timestamp': 'Časová značka', + 'history_change': 'Změna (GB)', + 'btn_fetch_now': 'Načíst nyní', + 'btn_clear_data': 'Vymazat všechna data', + 'btn_add_account': 'Přidat účet', + 'confirm_clear_data': 'Jste si jisti? Tímto smažete všechna historická data statistik.', + 'next_fetch': 'Další automatické načtení:', + 'login_header': 'Přihlaste se do svého panelu', + 'login_username': 'Administrátorské heslo', + 'login_password': 'Heslo', + 'login_button': 'Přihlásit se', + 'error_login_required': 'Pro zobrazení této stránky se musíte přihlásit.', + 'error_invalid_credentials': 'Neplatné heslo.', + 'flash_logged_in': 'Byli jste úspěšně přihlášeni.', + 'flash_logged_out': 'Byli jste odhlášeni.', + 'flash_settings_saved': 'Nastavení bylo úspěšně uloženo.', + 'flash_fetch_success': 'Nejnovější statistiky byly úspěšně načteny a uloženy.', + 'flash_fetch_fail': 'Nepodařilo se načíst statistiky: ', + 'flash_clear_success': 'Úspěšně smazáno {count} záznamů z databáze.', + 'flash_clear_fail': 'Při mazání databáze došlo k chybě.', + 'flash_account_added': 'Účet byl úspěšně přidán.', + 'flash_account_removed': 'Účet byl úspěšně odstraněn.', + 'flash_account_updated': 'Účet byl úspěšně aktualizován.', + 'webhook_title': 'Správa webhooků', + 'webhook_desc': 'Přidejte webhooky pro zasílání oznámení.', + 'webhook_url_label': 'URL adresa webhooku', + 'webhook_payload_label': 'Vlastní JSON Payload (volitelné)', + 'webhook_payload_placeholder': 'Např.: {"content": "Účet: ${account}, Data: ${total_gb} GB"}', + 'webhook_add_btn': 'Přidat Webhook', + 'webhook_current': 'Aktuální Webhooky', + 'webhook_delete_btn': 'Smazat', + 'webhook_flash_added': 'Webhook úspěšně přidán.', + 'webhook_flash_deleted': 'Webhook úspěšně smazán.', + 'webhook_flash_invalid_url': 'Je vyžadována platná URL adresa webhooku.', + 'webhook_flash_exists': 'Tato URL adresa webhooku je již zaregistrována.', + 'accounts_username': 'Uživatelské jméno', + 'accounts_nickname': 'Přezdívka', + 'accounts_status': 'Stav', + 'accounts_actions': 'Akce', + 'accounts_active': 'Aktivní', + 'accounts_inactive': 'Neaktivní', + 'accounts_toggle': 'Přepnout', + 'accounts_remove': 'Odebrat', + 'accounts_add_title': 'Přidat nový účet', + 'accounts_add_username': 'UrNetwork uživatelské jméno', + 'accounts_add_password': 'UrNetwork heslo', + 'accounts_add_nickname': 'Přezdívka (volitelné)', + 'combined_stats': 'Kombinovaná statistika', + 'individual_accounts': 'Jednotlivé účty', + 'view_combined': 'Zobrazit kombinovanou statistiku', + 'view_individual': 'Zobrazit jednotlivé účty', + 'map_title': 'Lokace poskytovatelů', + 'map_legend_title': 'Počet poskytovatelů', + 'map_legend_hover': 'Přejeďte myší přes zemi', + 'leaderboard_title': 'Žebříček', + 'leaderboard_rank': 'Pořadí', + 'leaderboard_name': 'Jméno sítě', + 'leaderboard_data': 'Poskytnutá data (MiB)', + 'devices_title': 'Stav a správa zařízení', + 'devices_status': 'Stav', + 'devices_name': 'Jméno zařízení', + 'devices_id': 'ID klienta', + 'devices_mode': 'Režim poskytování', + 'devices_account': 'Účet', + 'devices_remove': 'Odebrat', + 'devices_online': 'Online', + 'devices_offline': 'Offline', + 'devices_confirm_remove': 'Opravdu chcete odebrat toto zařízení?', + 'flash_device_removed': 'Zařízení úspěšně odebráno.', + 'flash_device_remove_fail': 'Nepodařilo se odebrat zařízení: ' + }, + 'en': { + 'nav_public': 'Public View', + 'nav_owner_dashboard': 'Owner Dashboard', + 'nav_overview': 'Overview', + 'nav_account': 'Account & Leaderboard', + 'nav_devices': 'Devices', + 'nav_settings': 'Settings', + 'nav_accounts': 'Account Management', + 'nav_logout': 'Logout', + 'title_public_dashboard': 'Public Dashboard', + 'title_owner_dashboard': 'Owner Dashboard', + 'title_account_page': 'Account & Leaderboard', + 'title_devices_page': 'Device Management', + 'title_settings': 'Settings', + 'title_accounts_page': 'Account Management', + 'title_login': 'Login', + 'title_setup': 'Initial Setup', + 'card_paid_data': 'Total Paid Data', + 'card_unpaid_data': 'Total Unpaid Data', + 'card_earnings_30d': 'Earnings (30 Days)', + 'card_total_earnings': 'Total Earnings', + 'card_last_update': 'Last Update', + 'card_account_points': 'Account Points', + 'card_your_rank': 'Your Rank', + 'card_total_referrals': 'Total Referrals', + 'card_active_accounts': 'Active Accounts', + 'card_total_devices': 'Total Devices', + 'chart_total_data_gb': 'Total Data Provided (GB)', + 'chart_paid_vs_unpaid': 'Paid vs. Unpaid Data (GB)', + 'chart_delta': 'Data Change per Interval (GB)', + 'chart_paid': 'Paid (GB)', + 'chart_unpaid': 'Unpaid (GB)', + 'chart_no_data': 'Not enough data to display a chart.', + 'history_title': 'Detailed History', + 'history_timestamp': 'Timestamp', + 'history_change': 'Change (GB)', + 'btn_fetch_now': 'Fetch Now', + 'btn_clear_data': 'Clear All Data', + 'btn_add_account': 'Add Account', + 'confirm_clear_data': 'Are you sure? This will delete all historical stats data.', + 'next_fetch': 'Next automatic fetch:', + 'login_header': 'Sign in to your dashboard', + 'login_username': 'Admin Password', + 'login_password': 'Password', + 'login_button': 'Sign in', + 'error_login_required': 'You must be logged in to view this page.', + 'error_invalid_credentials': 'Invalid password.', + 'flash_logged_in': 'You have been logged in successfully.', + 'flash_logged_out': 'You have been logged out.', + 'flash_settings_saved': 'Settings saved successfully.', + 'flash_fetch_success': 'Successfully fetched and saved latest stats.', + 'flash_fetch_fail': 'Failed to fetch stats: ', + 'flash_clear_success': 'Successfully cleared {count} records from the database.', + 'flash_clear_fail': 'An error occurred while clearing the database.', + 'flash_account_added': 'Account added successfully.', + 'flash_account_removed': 'Account removed successfully.', + 'flash_account_updated': 'Account updated successfully.', + 'webhook_title': 'Webhook Management', + 'webhook_desc': 'Add webhooks to send notifications.', + 'webhook_url_label': 'Webhook URL', + 'webhook_payload_label': 'Custom JSON Payload (optional)', + 'webhook_payload_placeholder': 'E.g.: {"content": "Account: ${account}, Data: ${total_gb} GB"}', + 'webhook_add_btn': 'Add Webhook', + 'webhook_current': 'Current Webhooks', + 'webhook_delete_btn': 'Delete', + 'webhook_flash_added': 'Webhook added successfully.', + 'webhook_flash_deleted': 'Webhook deleted successfully.', + 'webhook_flash_invalid_url': 'A valid Webhook URL is required.', + 'webhook_flash_exists': 'This webhook URL is already registered.', + 'accounts_username': 'Username', + 'accounts_nickname': 'Nickname', + 'accounts_status': 'Status', + 'accounts_actions': 'Actions', + 'accounts_active': 'Active', + 'accounts_inactive': 'Inactive', + 'accounts_toggle': 'Toggle', + 'accounts_remove': 'Remove', + 'accounts_add_title': 'Add New Account', + 'accounts_add_username': 'UrNetwork Username', + 'accounts_add_password': 'UrNetwork Password', + 'accounts_add_nickname': 'Nickname (optional)', + 'combined_stats': 'Combined Statistics', + 'individual_accounts': 'Individual Accounts', + 'view_combined': 'View Combined Stats', + 'view_individual': 'View Individual Accounts', + 'map_title': 'Provider Locations', + 'map_legend_title': 'Provider Count', + 'map_legend_hover': 'Hover over a country', + 'leaderboard_title': 'Leaderboard', + 'leaderboard_rank': 'Rank', + 'leaderboard_name': 'Network Name', + 'leaderboard_data': 'Data Provided (MiB)', + 'devices_title': 'Device Status & Management', + 'devices_status': 'Status', + 'devices_name': 'Device Name', + 'devices_id': 'Client ID', + 'devices_mode': 'Provide Mode', + 'devices_account': 'Account', + 'devices_remove': 'Remove', + 'devices_online': 'Online', + 'devices_offline': 'Offline', + 'devices_confirm_remove': 'Are you sure you want to remove this device?', + 'flash_device_removed': 'Device removed successfully.', + 'flash_device_remove_fail': 'Failed to remove device: ' + } +} + +def get_locale(): + """Detects the best language match from request headers.""" + return request.accept_languages.best_match(TEXTS.keys()) or 'en' + +# --- HTML Templates --- + +LAYOUT_TEMPLATE = """ + + + + + + {{ title }} + + + + + + + + + + +
+
+
+ + +
+
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + {{ content | safe }} +
+
+
+ + +""" + +INSTALL_TEMPLATE = """ +
+

{{ title }}

+

Nastavte administrátorské heslo pro váš dashboard.

+
+ + + +
+

Poté budete moci přidat své UrNetwork účty v nastavení.

+
+""" + +LOGIN_TEMPLATE = """ +
+

{{ title }}

+

{{ t.login_header }}

+
+ + +
+
+""" + +PUBLIC_DASHBOARD_TEMPLATE = """ +

{{ title }}

+
+
+
+
{{ t.card_paid_data }}
{{ "%.3f"|format(combined.paid_gb) }} GB
+
{{ t.card_unpaid_data }}
{{ "%.3f"|format(combined.unpaid_gb) }} GB
+
{{ t.card_active_accounts }}
{{ active_accounts }}
+
{{ t.card_earnings_30d }}
${{ "%.2f"|format(monthly_earnings) }}
+
+
+
+
+

{{ t.chart_total_data_gb }} - {{ t.combined_stats }}

+
+ {% if chart_data.labels %}{% else %}

{{ t.chart_no_data }}

{% endif %} +
+
+ + {% for account, acc_data in account_charts.items() %} +
+

{{ t.chart_total_data_gb }}

+
+ {% if acc_data.labels %}{% else %}

{{ t.chart_no_data }}

{% endif %} +
+
+ {% endfor %} + +
+

{{ t.map_title }}

+
+
+
+
+ +""" + +ACCOUNTS_MANAGE_TEMPLATE = """ +

{{ title }}

+
+
+

{{ t.accounts_add_title }}

+
+ + + + + + + + + + +
+
+ +
+

{{ t.individual_accounts }}

+ + + + + + + + + + + {% for account in accounts %} + + + + + + + {% else %} + + + + {% endfor %} + +
{{ t.accounts_nickname }}{{ t.accounts_username }}{{ t.accounts_status }}{{ t.accounts_actions }}
{{ account.username }} + + {{ t.accounts_active if account.is_active else t.accounts_inactive }} + + +
+ +
+
+ +
+
Zatím nemáte přidané žádné účty.
+
+
+""" + +PRIVATE_DASHBOARD_REACT_TEMPLATE = """ + + + + +
+ + +""" + +SETTINGS_TEMPLATE = """ +

{{ title }}

+
+
+

{{ t.webhook_title }}

+

{{ t.webhook_desc }}

+
+ + + + + +
+
+
+

{{ t.webhook_current }}

+ {% for webhook in webhooks %} +
+

{{ webhook.url }}

+
{{ webhook.payload or 'Default Discord Payload' }}
+
+ +
+
+ {% else %} +

Zatím nemáte nakonfigurované žádné webhooky.

+ {% endfor %} +
+
+""" + +# World map data +WORLD_MAP_DATA = None +try: + map_url = "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson" + response = requests.get(map_url, timeout=15) + response.raise_for_status() + WORLD_MAP_DATA = response.json() + logging.info("Successfully loaded world map GeoJSON data.") +except Exception as e: + logging.error(f"Could not download world map data: {e}") + +def render_page(template_content, **context): + """Renders a page by injecting its content into the main layout.""" + context['world_map_data'] = WORLD_MAP_DATA + content = render_template_string(template_content, t=g.t, **context) + return render_template_string(LAYOUT_TEMPLATE, content=content, t=g.t, **context) + +# --- Middleware and Before Request Handlers --- + +@app.before_request +def force_secure(): + """Redirect non-secure requests to HTTPS if FORCE_HTTPS is enabled.""" + if app.config['FORCE_HTTPS'] and not request.is_secure: + if request.headers.get('X-Forwarded-Proto', '').lower() != 'https': + secure_url = request.url.replace('http://', 'https://', 1) + return redirect(secure_url, code=301) + +@app.before_request +def check_installation_and_init(): + """Before each request, check if app is installed and set up globals.""" + g.locale = get_locale() + g.t = TEXTS[g.locale] + + if request.endpoint not in ['install', 'static'] and not is_installed(): + return redirect(url_for('install')) + + if is_installed() and request.endpoint not in ['static']: + with app.app_context(): + if not Setting.query.first(): + logging.info("First run: Initializing settings.") + settings_to_add = [ + Setting(key='ENABLE_ACCOUNT_STATS', value=str(app.config['ENABLE_ACCOUNT_STATS'])), + Setting(key='ENABLE_LEADERBOARD', value=str(app.config['ENABLE_LEADERBOARD'])), + Setting(key='ENABLE_DEVICE_STATS', value=str(app.config['ENABLE_DEVICE_STATS'])), + ] + db.session.bulk_save_objects(settings_to_add) + db.session.commit() + + g.now = datetime.datetime.utcnow() + +@app.context_processor +def inject_layout_vars(): + """Inject variables into all templates.""" + return {"now": g.now, "t": g.t} + +# --- Routes --- + +@app.route('/install', methods=['GET', 'POST']) +def install(): + if is_installed(): + flash("Aplikace je již nainstalována.", "info") + return redirect(url_for('public_dashboard')) + + if request.method == 'POST': + admin_pass = request.form.get('admin_pass') + admin_pass_confirm = request.form.get('admin_pass_confirm') + + if not admin_pass or not admin_pass_confirm: + flash("Vyplňte obě pole s heslem.", "error") + return render_page(INSTALL_TEMPLATE, title=g.t['title_setup']) + + if admin_pass != admin_pass_confirm: + flash("Hesla se neshodují.", "error") + return render_page(INSTALL_TEMPLATE, title=g.t['title_setup']) + + config_data = { + "SECRET_KEY": secrets.token_hex(24), + "ADMIN_PASSWORD": admin_pass + } + + if save_env_file(config_data): + with app.app_context(): + db.create_all() + session['logged_in'] = True + flash("Dashboard byl úspěšně nainstalován! Nyní můžete přidat své UrNetwork účty.", "info") + return redirect(url_for('accounts_manage')) + else: + flash("Chyba při ukládání konfigurace.", "error") + + return render_page(INSTALL_TEMPLATE, title=g.t['title_setup']) + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if session.get('logged_in'): + return redirect(url_for('private_dashboard')) + + if request.method == 'POST': + password = request.form.get('password') + admin_password = os.getenv('ADMIN_PASSWORD') + + if password and password == admin_password: + session['logged_in'] = True + flash(g.t['flash_logged_in'], 'info') + next_url = request.args.get('next') + return redirect(next_url or url_for('private_dashboard')) + else: + flash(g.t['error_invalid_credentials'], 'error') + + return render_page(LOGIN_TEMPLATE, title=g.t['title_login']) + +@app.route('/logout') +def logout(): + session.pop('logged_in', None) + flash(g.t['flash_logged_out'], 'info') + return redirect(url_for('public_dashboard')) + +@app.route('/') +def public_dashboard(): + """Display the public-facing dashboard with combined stats and individual account charts.""" + accounts = Account.query.filter_by(is_active=True).all() + active_accounts = len(accounts) + + # Get combined statistics + combined_paid = 0 + combined_unpaid = 0 + monthly_earnings_total = 0 + + account_charts = {} + colors = ['#667eea', '#764ba2', '#f59e0b', '#10b981', '#ef4444', '#3b82f6'] + + for idx, account in enumerate(accounts): + latest_stat = Stats.query.filter_by(account_id=account.id).order_by(Stats.timestamp.desc()).first() + if latest_stat: + combined_paid += latest_stat.paid_gb + combined_unpaid += latest_stat.unpaid_gb + + # Get JWT and payments + jwt = get_valid_jwt(account) + if jwt: + payments = fetch_payment_stats(jwt) + _, monthly = calculate_earnings(payments) + monthly_earnings_total += monthly + + # Get chart data for this account + entries = Stats.query.filter_by(account_id=account.id).order_by(Stats.timestamp.asc()).all() + if entries: + account_name = account.nickname or account.username + color = colors[idx % len(colors)] + account_charts[account_name] = { + "labels": [e.timestamp.strftime('%m-%d %H:%M') for e in entries], + "data": [e.paid_gb + e.unpaid_gb for e in entries], + "color": color, + "bgColor": f"rgba({int(color[1:3], 16)}, {int(color[3:5], 16)}, {int(color[5:7], 16)}, 0.2)" + } + + combined = { + "paid_gb": combined_paid, + "unpaid_gb": combined_unpaid + } + + # Combined chart data + all_entries = Stats.query.order_by(Stats.timestamp.asc()).all() + time_grouped = {} + for entry in all_entries: + time_key = entry.timestamp.strftime('%m-%d %H:%M') + if time_key not in time_grouped: + time_grouped[time_key] = {"paid": 0, "unpaid": 0} + time_grouped[time_key]["paid"] += entry.paid_gb + time_grouped[time_key]["unpaid"] += entry.unpaid_gb + + chart_data = { + "labels": list(time_grouped.keys()), + "data": [time_grouped[k]["paid"] + time_grouped[k]["unpaid"] for k in time_grouped.keys()] + } + + return render_page( + PUBLIC_DASHBOARD_TEMPLATE, + title=g.t['title_public_dashboard'], + combined=combined, + active_accounts=active_accounts, + monthly_earnings=monthly_earnings_total, + chart_data=chart_data, + account_charts=account_charts + ) + +@app.route('/dashboard') +@login_required +def private_dashboard(): + react_props = get_react_props('overview') + return render_page(PRIVATE_DASHBOARD_REACT_TEMPLATE, title=g.t['title_owner_dashboard'], react_props=react_props) + +def get_react_props(initial_page): + """Helper to build the props dictionary for the React app.""" + return { + "initial_page": initial_page, + "translations": g.t + } + +# --- Account Management Routes --- + +@app.route('/accounts') +@login_required +def accounts_manage(): + accounts = Account.query.all() + return render_page(ACCOUNTS_MANAGE_TEMPLATE, title=g.t['title_accounts_page'], accounts=accounts) + +@app.route('/accounts/add', methods=['POST']) +@login_required +def accounts_add(): + username = request.form.get('username') + password = request.form.get('password') + nickname = request.form.get('nickname') + + if not username or not password: + flash("Uživatelské jméno a heslo jsou povinné.", "error") + return redirect(url_for('accounts_manage')) + + # Verify credentials + jwt = get_jwt_from_credentials(username, password) + if not jwt: + flash("Nepodařilo se ověřit přihlašovací údaje. Zkontrolujte je a zkuste to znovu.", "error") + return redirect(url_for('accounts_manage')) + + # Check if account already exists + existing = Account.query.filter_by(username=username).first() + if existing: + flash("Tento účet je již přidán.", "error") + return redirect(url_for('accounts_manage')) + + account = Account(username=username, password=password, nickname=nickname) + db.session.add(account) + db.session.commit() + + flash(g.t['flash_account_added'], "info") + return redirect(url_for('accounts_manage')) + +@app.route('/accounts/toggle/', methods=['POST']) +@login_required +def accounts_toggle(account_id): + account = Account.query.get_or_404(account_id) + account.is_active = not account.is_active + db.session.commit() + flash(g.t['flash_account_updated'], "info") + return redirect(url_for('accounts_manage')) + +@app.route('/accounts/remove/', methods=['POST']) +@login_required +def accounts_remove(account_id): + account = Account.query.get_or_404(account_id) + # Delete all stats for this account + Stats.query.filter_by(account_id=account_id).delete() + db.session.delete(account) + db.session.commit() + flash(g.t['flash_account_removed'], "info") + return redirect(url_for('accounts_manage')) + +# --- API Routes for React App --- + +@app.route('/api/dashboard/overview') +@login_required +def api_overview_data(): + accounts = Account.query.filter_by(is_active=True).all() + + # Combined statistics + combined_paid = 0 + combined_unpaid = 0 + total_earnings = 0 + + account_charts = {} + colors = ['#667eea', '#764ba2', '#f59e0b', '#10b981', '#ef4444', '#3b82f6'] + + for idx, account in enumerate(accounts): + # Get latest stat + latest_stat = Stats.query.filter_by(account_id=account.id).order_by(Stats.timestamp.desc()).first() + if latest_stat: + combined_paid += latest_stat.paid_gb + combined_unpaid += latest_stat.unpaid_gb + + # Get earnings + jwt = get_valid_jwt(account) + if jwt: + payments = fetch_payment_stats(jwt) + total, _ = calculate_earnings(payments) + total_earnings += total + + # Get chart data + entries = Stats.query.filter_by(account_id=account.id).order_by(Stats.timestamp.asc()).all() + if entries: + account_name = account.nickname or account.username + color = colors[idx % len(colors)] + account_charts[account_name] = { + "labels": [e.timestamp.strftime('%m-%d %H:%M') for e in entries], + "data": [e.paid_gb + e.unpaid_gb for e in entries], + "color": color, + "bgColor": f"rgba({int(color[1:3], 16)}, {int(color[3:5], 16)}, {int(color[5:7], 16)}, 0.2)" + } + + # Combined chart + all_entries = Stats.query.order_by(Stats.timestamp.asc()).all() + time_grouped = {} + for entry in all_entries: + time_key = entry.timestamp.strftime('%m-%d %H:%M') + if time_key not in time_grouped: + time_grouped[time_key] = {"paid": 0, "unpaid": 0} + time_grouped[time_key]["paid"] += entry.paid_gb + time_grouped[time_key]["unpaid"] += entry.unpaid_gb + + combined_chart = { + "labels": list(time_grouped.keys()), + "paid_gb": [time_grouped[k]["paid"] for k in time_grouped.keys()], + "unpaid_gb": [time_grouped[k]["unpaid"] for k in time_grouped.keys()] + } + + return jsonify({ + "combined": { + "paid_gb": combined_paid, + "unpaid_gb": combined_unpaid + }, + "active_accounts": len(accounts), + "total_earnings": total_earnings, + "combined_chart": combined_chart, + "account_charts": account_charts + }) + +@app.route('/api/dashboard/account') +@login_required +def api_account_data(): + account_id = request.args.get('account_id', 'all') + + accounts = Account.query.filter_by(is_active=True).all() + account_details = {} + leaderboard = [] + + if account_id != 'all': + account = Account.query.get(int(account_id)) + if account: + jwt = get_valid_jwt(account) + if jwt: + account_details = fetch_account_details(jwt) + leaderboard = fetch_leaderboard(jwt) + + return jsonify({ + "accounts": [{"id": a.id, "username": a.username, "nickname": a.nickname} for a in accounts], + "account_details": account_details, + "leaderboard": leaderboard + }) + +@app.route('/api/dashboard/devices') +@login_required +def api_devices_data(): + accounts = Account.query.filter_by(is_active=True).all() + all_devices = [] + + for account in accounts: + jwt = get_valid_jwt(account) + if jwt: + devices = fetch_devices(jwt) + for device in devices: + device['account_id'] = account.id + device['account_nickname'] = account.nickname or account.username + all_devices.extend(devices) + + return jsonify({"devices": all_devices}) + +@app.route('/api/dashboard/devices/remove//', methods=["POST"]) +@login_required +def api_remove_device(account_id, client_id): + account = Account.query.get(account_id) + if not account: + return jsonify({"success": False, "message": "Account not found"}), 404 + + jwt = get_valid_jwt(account) + success, message = remove_device(jwt, client_id) + + if success: + return jsonify({"success": True, "message": g.t['flash_device_removed']}) + else: + return jsonify({"success": False, "message": f"{g.t['flash_device_remove_fail']}{message}"}), 500 + +# --- Other Routes --- + +@app.route('/api/locations') +def get_locations(): + """API endpoint to proxy provider locations.""" + data = fetch_provider_locations() + if data: return jsonify(data) + return jsonify({"error": "Failed to fetch location data"}), 500 + +@app.route('/settings', methods=['GET', 'POST']) +@login_required +def settings(): + webhooks = Webhook.query.all() + return render_page(SETTINGS_TEMPLATE, title=g.t['title_settings'], webhooks=webhooks) + +@app.route("/webhooks/add", methods=["POST"]) +@login_required +def add_webhook(): + """Add a new webhook.""" + url = request.form.get("webhook_url") + payload = request.form.get("payload") + if not url or not (url.startswith("http://") or url.startswith("https://")): + flash(g.t['webhook_flash_invalid_url'], "error") + elif Webhook.query.filter_by(url=url).first(): + flash(g.t['webhook_flash_exists'], "error") + else: + new_webhook = Webhook(url=url, payload=payload if payload.strip() else None) + db.session.add(new_webhook) + db.session.commit() + flash(g.t['webhook_flash_added'], "info") + return redirect(url_for('settings')) + +@app.route("/webhooks/delete/", methods=["POST"]) +@login_required +def delete_webhook(webhook_id): + """Delete a webhook.""" + webhook = Webhook.query.get(webhook_id) + if webhook: + db.session.delete(webhook) + db.session.commit() + flash(g.t['webhook_flash_deleted'], "info") + return redirect(url_for('settings')) + +# --- Main Entry Point --- +if __name__ == "__main__": + with app.app_context(): + db.create_all() + scheduler.start() + app.run(host="0.0.0.0", port=90, debug=False) diff --git a/migrate.py b/migrate.py new file mode 100644 index 0000000..b400e66 --- /dev/null +++ b/migrate.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +""" +Database Migration Script for UrNetwork Stats Dashboard v2.0 +Migrates from single-account to multi-account structure +""" + +import sqlite3 +import os +import sys +from getpass import getpass + +def print_header(text): + print("\n" + "=" * 60) + print(f" {text}") + print("=" * 60) + +def print_success(text): + print(f"✓ {text}") + +def print_warning(text): + print(f"⚠ {text}") + +def print_error(text): + print(f"✗ {text}") + +def backup_files(): + """Create backups of existing files""" + print_header("Vytváření záloh") + + if os.path.exists('.env'): + os.system('cp .env .env.backup') + print_success("Zazálohován .env → .env.backup") + + # Check for database in multiple locations + db_locations = ['transfer_stats.db', 'instance/transfer_stats.db'] + for db_path in db_locations: + if os.path.exists(db_path): + backup_path = db_path + '.backup' + os.system(f'cp {db_path} {backup_path}') + print_success(f"Zazálohována databáze → {backup_path}") + break + +def migrate_database(): + """Migrate database to new structure""" + print_header("Migrace databáze") + + # Check for database in multiple locations + db_path = None + possible_paths = [ + 'transfer_stats.db', + 'instance/transfer_stats.db', + '../transfer_stats.db' + ] + + for path in possible_paths: + if os.path.exists(path): + db_path = path + print_success(f"Nalezena databáze: {db_path}") + break + + if not db_path: + print_error("Databáze transfer_stats.db nenalezena!") + print_warning("Hledáno v: " + ", ".join(possible_paths)) + return False + + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 1. Create accounts table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + nickname TEXT, + is_active BOOLEAN DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + print_success("Vytvořena tabulka accounts") + + # 2. Add account_id column to stats + try: + cursor.execute('ALTER TABLE stats ADD COLUMN account_id INTEGER') + print_success("Přidán sloupec account_id do tabulky stats") + except sqlite3.OperationalError as e: + if "duplicate column" in str(e).lower(): + print_warning("Sloupec account_id již existuje") + else: + raise + + # 3. Migrate existing account from .env + if os.path.exists('.env'): + username = None + password = None + + with open('.env', 'r') as f: + for line in f: + if line.startswith('UR_USER='): + username = line.split('=', 1)[1].strip() + elif line.startswith('UR_PASS='): + password = line.split('=', 1)[1].strip() + + if username and password: + cursor.execute('SELECT COUNT(*) FROM accounts WHERE username = ?', (username,)) + + if cursor.fetchone()[0] == 0: + print(f"\nNalezen existující účet: {username}") + nickname = input("Zadejte přezdívku pro tento účet (nebo stiskněte Enter pro přeskočení): ").strip() + + cursor.execute( + 'INSERT INTO accounts (username, password, nickname, is_active) VALUES (?, ?, ?, 1)', + (username, password, nickname if nickname else None) + ) + account_id = cursor.lastrowid + + # Update existing stats + cursor.execute('UPDATE stats SET account_id = ? WHERE account_id IS NULL', (account_id,)) + updated_count = cursor.rowcount + + print_success(f"Migrován účet: {username}") + if nickname: + print_success(f" Přezdívka: {nickname}") + print_success(f" Aktualizováno {updated_count} statistických záznamů") + else: + print_warning(f"Účet {username} již existuje v databázi") + else: + print_warning("Nenalezeny credentials v .env souboru") + else: + print_warning(".env soubor nenalezen") + + conn.commit() + conn.close() + + print_success("Migrace databáze dokončena!") + return True + + except Exception as e: + print_error(f"Chyba při migraci databáze: {e}") + return False + +def update_env_file(): + """Update .env file with admin password""" + print_header("Aktualizace .env souboru") + + if not os.path.exists('.env'): + print_warning(".env soubor nenalezen, bude vytvořen nový") + env_lines = [] + else: + with open('.env', 'r') as f: + env_lines = f.readlines() + + # Check if ADMIN_PASSWORD exists + has_admin_pass = any(line.startswith('ADMIN_PASSWORD=') for line in env_lines) + + if has_admin_pass: + print_warning("ADMIN_PASSWORD již existuje v .env") + response = input("Chcete nastavit nové heslo? (y/n): ").lower() + if response != 'y': + return True + + # Remove old password + env_lines = [line for line in env_lines if not line.startswith('ADMIN_PASSWORD=')] + + print("\nNastavte administrátorské heslo pro přístup do dashboardu.") + print("(Toto je oddělené od vašich UrNetwork credentials)") + + while True: + admin_pass = getpass("\nAdministrátorské heslo: ") + admin_pass_confirm = getpass("Potvrďte heslo: ") + + if admin_pass == admin_pass_confirm: + if len(admin_pass) < 6: + print_error("Heslo musí mít alespoň 6 znaků!") + continue + break + else: + print_error("Hesla se neshodují!") + + # Add admin password + if not any(line.strip() == "# Admin Access" for line in env_lines): + env_lines.insert(0, "# Admin Access\n") + env_lines.insert(1, f"ADMIN_PASSWORD={admin_pass}\n") + env_lines.insert(2, "\n") + + # Write updated .env + with open('.env', 'w') as f: + f.writelines(env_lines) + + print_success("Administrátorské heslo nastaveno") + return True + +def verify_migration(): + """Verify that migration was successful""" + print_header("Ověření migrace") + + # Find database + db_path = None + for path in ['transfer_stats.db', 'instance/transfer_stats.db']: + if os.path.exists(path): + db_path = path + break + + if not db_path: + print_error("Databáze nenalezena pro ověření") + return False + + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Check accounts table + cursor.execute("SELECT COUNT(*) FROM accounts") + account_count = cursor.fetchone()[0] + print_success(f"Nalezeno {account_count} účtů v databázi") + + # Check stats with account_id + cursor.execute("SELECT COUNT(*) FROM stats WHERE account_id IS NOT NULL") + stats_count = cursor.fetchone()[0] + print_success(f"Nalezeno {stats_count} statistických záznamů s account_id") + + # Check orphaned stats + cursor.execute("SELECT COUNT(*) FROM stats WHERE account_id IS NULL") + orphaned_count = cursor.fetchone()[0] + if orphaned_count > 0: + print_warning(f"Nalezeno {orphaned_count} statistik bez přiřazeného účtu") + + conn.close() + + # Check .env + if os.path.exists('.env'): + with open('.env', 'r') as f: + env_content = f.read() + if 'ADMIN_PASSWORD=' in env_content: + print_success("ADMIN_PASSWORD nalezeno v .env") + else: + print_warning("ADMIN_PASSWORD chybí v .env") + + return True + + except Exception as e: + print_error(f"Chyba při ověření: {e}") + return False + +def print_next_steps(): + """Print next steps after migration""" + print_header("Další kroky") + print(""" +1. Restartujte aplikaci: + pkill -f main.py + python3 main.py + +2. Přihlaste se pomocí nového admin hesla + +3. Přejděte do "Správa účtů" pro přidání dalších UrNetwork účtů + +4. Pokud něco nefunguje, obnovte zálohy: + cp .env.backup .env + cp transfer_stats.db.backup transfer_stats.db + +Dokumentace: README_CZ.md + """) + +def main(): + print(""" +╔════════════════════════════════════════════════════════════╗ +║ UrNetwork Stats Dashboard - Migration Script v2.0 ║ +║ Migrace z single-account na multi-account strukturu ║ +╚════════════════════════════════════════════════════════════╝ + """) + + print("\nTento skript provede následující:") + print(" • Vytvoří zálohy .env a databáze") + print(" • Přidá podporu pro více účtů") + print(" • Nastaví administrátorské heslo") + print(" • Migruje existující data") + + response = input("\nPokračovat? (y/n): ").lower() + if response != 'y': + print("Migrace zrušena.") + sys.exit(0) + + # Run migration steps + backup_files() + + if not migrate_database(): + print_error("\nMigrace selhala při aktualizaci databáze!") + print("Obnovte zálohy a zkuste to znovu.") + sys.exit(1) + + if not update_env_file(): + print_error("\nMigrace selhala při aktualizaci .env!") + print("Obnovte zálohy a zkuste to znovu.") + sys.exit(1) + + if not verify_migration(): + print_warning("\nOvěření migrace selhalo, ale data by měla být OK") + + print_next_steps() + + print_header("Migrace úspěšně dokončena! 🎉") + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n\nMigrace přerušena uživatelem.") + sys.exit(1) + except Exception as e: + print_error(f"\nNeočekávaná chyba: {e}") + print("Obnovte zálohy pomocí:") + print(" cp .env.backup .env") + print(" cp transfer_stats.db.backup transfer_stats.db") + sys.exit(1) diff --git a/readme.md b/readme.md index 5a0a5a1..5f6b6cd 100644 --- a/readme.md +++ b/readme.md @@ -1,275 +1,401 @@ -# UrNetwork Stats Dashboard - Refactored - +# UrNetwork Stats Dashboard - Enhanced Multi-Account Edition v2.1 Hits - [demo](http://38.242.156.120:90) | [![Hits](https://hits.sh/38.242.156.120:90.svg)](https://hits.sh/38.242.156.120:90/) Hits - [repo](https://forgejo.plainrock127.xyz/mxnticek/UrNetwork-Stats-Dashboard-remade) | [![Hits](https://hits.sh/forgejo.plainrock127.xyz/mxnticek/UrNetwork-Stats-Dashboard-remade.svg)](https://hits.sh/forgejo.plainrock127.xyz/mxnticek/UrNetwork-Stats-Dashboard-remade/) +--- ------ +## 🌟 Quick Links + +- 🚀 [Quick Start Guide](QUICKSTART.md) - Get running in 5 minutes +- 📊 [Webhook Setup Tutorial](WEBHOOK_GUIDE.md) - Discord, Slack, Telegram examples +- 🎨 [Design Comparison](DESIGN_COMPARISON.md) - Why this design? +- 🔄 [Upgrade Guide](UPGRADE_GUIDE.md) - Migrating from v1.0 +- 📝 [Changelog](CHANGELOG.md) - Version history +- 🇨🇿 [Český README](README_CZ.md) - Czech documentation + +--- ## 🇬🇧 English ### About The Project -This project is a significantly enhanced and refactored version of the original [UrNetwork Stats Dashboard](https://github.com/techroy23/UrNetwork-Stats-Dashboard). The application is designed to track, store, and visualize statistics about data transfers (both paid and unpaid) and earnings from your UrNetwork account. +This is a **significantly enhanced multi-account version** of the original [UrNetwork Stats Dashboard](https://github.com/techroy23/UrNetwork-Stats-Dashboard). Track unlimited UrNetwork accounts with beautiful charts, real-time updates, and webhook notifications. -The entire application is packaged into a single `main_app.py` file for simplicity, yet it contains robust features including a web-based installer, authentication, public/private views, interactive charts, a provider map, and support for webhook notifications. +**What's New in v2.1:** +- 🎯 **Multi-Account Support** - Track unlimited UrNetwork accounts +- 🎨 **Clean Design** - Cloudflare-inspired dark theme +- 📊 **Better Charts** - White text, tooltips anywhere on chart +- 🔐 **Separate Auth** - Admin password independent from UrNetwork +- ⚡ **Daily Cleanup** - Auto-delete data older than 7 days +- 📱 **Mobile Optimized** - Touch-friendly interface -> **A Note from the Author** -> -> I know I might get some hate for using AI to build and refactor this, but since I have access to it for free, I want to make the most of it. Plus, no offense to my friends who can code, but Gemini often explains concepts more clearly! (no hate) -> (this readme is made by ai too.... please just dont hate me, im a stupid bitch-ass idiot that's almost braindead and im not even kidding.) +> **Enhanced by AI:** v2.0+ developed with Claude (Anthropic) for better UX, multi-account support, and modern design. ------ +--- -### ✨ Features +### ✨ Key Features - * **🐍 All-in-One Python Script:** The entire application logic, including the frontend, is contained within a single `main_app.py` file. - * **🚀 Web-Based Installer:** A simple setup wizard guides you through configuring your UrNetwork API credentials on the first run. - * **🔐 Dual Views (Public/Private):** - * **Public Dashboard:** Displays summary stats, total data over time, and a world map of provider locations. Perfect for a quick overview. - * **Private Dashboard:** After logging in, you get access to detailed charts, a complete history of data points, webhook management, and other administrative functions. - * **📊 Interactive Charts:** Utilizes `Chart.js` for data visualization, including: - * Total data provided over time. - * Paid vs. Unpaid data comparison. - * A bar chart showing data change between intervals. - * **🗺️ Provider Map:** Displays the number of active providers by country on a world map using `Leaflet.js`. - * **⚙️ Automated Data Collection:** A scheduler (`APScheduler`) automatically fetches and saves new data from the API every 15 minutes. - * **🔔 Webhook Notifications:** Add your own webhook URLs (e.g., for Discord) to receive notifications with each new data sync. - * **🎨 Modern UI:** - * Styled with **Tailwind CSS**. - * **Light/Dark/System theme** support. - * Fully responsive design. - * **💾 Persistent Storage:** Uses `SQLAlchemy` and a `SQLite` database to store historical data. +#### 🎯 Multi-Account Management +- ✅ Track unlimited UrNetwork accounts simultaneously +- ✅ Combined statistics from all accounts +- ✅ Individual charts for each account +- ✅ Custom nicknames for easy identification +- ✅ Toggle accounts on/off without deletion +- ✅ Color-coded for easy distinction ------ +#### 📊 Advanced Visualization +- ✅ Interactive Chart.js charts with tooltips **anywhere on chart** +- ✅ Total data, Paid vs Unpaid, Delta charts +- ✅ White readable text on all charts (no more squinting!) +- ✅ World map showing provider locations +- ✅ Real-time updates every minute -### 🚀 Getting Started +#### ⚙️ Automation +- ✅ Data collection every 15 minutes +- ✅ Daily cleanup (keeps last 7 days) +- ✅ Webhook notifications (Discord, Slack, Telegram) +- ✅ Device management across all accounts -#### Prerequisites +#### 🎨 Modern UI +- ✅ Clean Cloudflare-inspired dark theme +- ✅ No gradients (better chart visibility) +- ✅ Blue accent color (#3b82f6) +- ✅ Fully responsive (mobile, tablet, desktop) +- ✅ Czech & English support - * Python 3.x - * Pip (Python package manager) +--- -#### Installation +### 🚀 Quick Start -1. **Clone the repository:** +```bash +# 1. Clone repository +git clone https://forgejo.plainrock127.xyz/mxnticek/UrNetwork-Stats-Dashboard-remade +cd UrNetwork-Stats-Dashboard-remade - ```bash - git clone - cd - ``` +# 2. Install dependencies +pip install Flask Flask-SQLAlchemy Flask-APScheduler requests python-dateutil -2. **Create and activate a virtual environment (recommended):** +# 3. Run +python main.py - ```bash - python -m venv venv - # On Windows - .venv\Scripts\activate - # On macOS / Linux - source venv/bin/activate - ``` +# 4. Open browser +# http://localhost:90 +``` -3. **Install requirements:** - Create a `requirements.txt` file with the following content: +**First Run Setup:** +1. Set your **admin password** (NOT your UrNetwork password!) +2. Add your UrNetwork accounts (email + password + nickname) +3. Done! Dashboard will start collecting data every 15 minutes - ```txt - Flask - Flask-SQLAlchemy - Flask-APScheduler - requests - python-dateutil - gunicorn - ``` +📖 **Need more details?** See [QUICKSTART.md](QUICKSTART.md) - Then, install them: +--- - ```bash - pip install -r requirements.txt - ``` +### 📊 Webhook Setup - Quick Examples -4. **Run the application:** +#### Discord - Simple +```json +{ + "content": "📊 **${account}**: ${total_gb} GB" +} +``` - ```bash - python main_app.py - ``` +#### Discord - Rich Embed (Recommended) +```json +{ + "username": "UrNetwork Bot", + "embeds": [{ + "title": "⚡ ${account} - New Data!", + "color": 3901635, + "fields": [ + {"name": "💰 Paid", "value": "`${paid_gb} GB`", "inline": true}, + {"name": "📡 Unpaid", "value": "`${unpaid_gb} GB`", "inline": true}, + {"name": "💾 Total", "value": "**${total_gb} GB**", "inline": false} + ], + "footer": {"text": "UrNetwork Stats"}, + "timestamp": "${update_time}" + }] +} +``` - *Note: For a production environment, it's better to use a WSGI server like `gunicorn`.* +#### Telegram +```json +{ + "chat_id": "YOUR_CHAT_ID", + "text": "📊 *${account}*\n💾 Total: ${total_gb} GB\n💰 Paid: ${paid_gb} GB\n📡 Unpaid: ${unpaid_gb} GB", + "parse_mode": "Markdown" +} +``` - ```bash - gunicorn --bind 0.0.0.0:90 main_app:app - ``` +**Available Variables:** +- `${account}` - Account nickname +- `${paid_gb}` - Paid data in GB +- `${unpaid_gb}` - Unpaid data in GB +- `${total_gb}` - Total data in GB +- `${update_time}` - Timestamp -5. **Web-based Configuration:** +📖 **Full webhook guide:** [WEBHOOK_GUIDE.md](WEBHOOK_GUIDE.md) - * Open `http://localhost:90` (or your server's IP address) in your browser. - * You will be redirected to the setup page. Enter your UrNetwork **username** and **password**. - * Upon successful verification, a `.env` file will be created with your credentials and a secret key will be generated for the session. The application is now installed. +--- ------ +### 🎨 Why This Design? -### 🕹️ Usage +After extensive testing, we chose a **clean Cloudflare-inspired design** because: -After installation, the application is ready to use. +1. **Better Chart Visibility** - Solid dark background = better contrast +2. **Professional Look** - Mimics enterprise dashboards +3. **No Eye Strain** - Pure colors without gradients +4. **Performance** - No blur effects = faster rendering +5. **Accessibility** - High contrast ratios - * **Public Dashboard (`/`):** The main page, accessible to anyone. It shows summary stats and the provider map. The data on this page updates automatically every minute without a page refresh. - * **Private Dashboard (`/dashboard`):** - * Click on "Owner Dashboard" to access it. - * Log in using the same credentials you entered during setup. - * Here you will find detailed charts, the full history of all measurements, and you can manage your webhooks. - * **Fetch Now:** Manually triggers a data fetch from the API. - * **Clear All Data:** Deletes all historical data from the database. - * **Webhook Management:** Add or remove URLs for notifications. +**Color Scheme:** +```css +Background: #0c0d0e (Deep dark) +Primary: #3b82f6 (Blue) +Text: #e5e7eb (Light) +Success: #10b981 (Green) +Danger: #ef4444 (Red) +``` ------ +📖 **Design details:** [DESIGN_COMPARISON.md](DESIGN_COMPARISON.md) + +--- + +### 📁 Project Structure + +``` +urnetwork-stats/ +├── main.py # Main application (use this!) +├── instance/ +│ └── transfer_stats.db # SQLite database (auto-created) +├── .env # Config (auto-created) +├── README.md # This file +├── requirements.txt # Dependencies +└── docs/ + ├── QUICKSTART.md # Quick start guide + ├── WEBHOOK_GUIDE.md # Webhook examples + ├── UPGRADE_GUIDE.md # Migration from v1.0 + ├── DESIGN_COMPARISON.md # Design documentation + └── CHANGELOG.md # Version history +``` + +--- ### 🛠️ Tech Stack - * **Backend:** [Flask](https://flask.palletsprojects.com/) - * **Database:** [Flask-SQLAlchemy](https://flask-sqlalchemy.palletsprojects.com/) (with SQLite) - * **Job Scheduler:** [Flask-APScheduler](https://github.com/viniciuschiele/flask-apscheduler) - * **Frontend:** [Tailwind CSS](https://tailwindcss.com/), [Chart.js](https://www.chartjs.org/), [Leaflet.js](https://leafletjs.com/) - * **HTTP Requests:** [Requests](https://requests.readthedocs.io/en/latest/) +- **Backend:** Flask 3.0+ +- **Database:** SQLAlchemy + SQLite +- **Scheduler:** APScheduler +- **Frontend:** React 18 (private dashboard) +- **Charts:** Chart.js 4.0+ +- **Maps:** Leaflet.js 1.9+ +- **Styling:** Custom CSS (Cloudflare-inspired) ------ +--- -### 🙏 Acknowledgements +### 🔄 Upgrading from v1.0 - * Original concept and project by **[techroy23](https://github.com/techroy23/UrNetwork-Stats-Dashboard)**. - * This version was completely refactored and extended with the help of AI. +```bash +# 1. Backup +cp .env .env.backup +cp instance/transfer_stats.db instance/transfer_stats.db.backup -
-
-
+# 2. Download new version +git pull -## 🇨🇿 Česky +# 3. Run migration +python migrate.py -### O Projektu +# 4. Restart +python main.py +``` -Tento projekt je výrazně vylepšená a refaktorovaná verze původního [UrNetwork Stats Dashboard](https://github.com/techroy23/UrNetwork-Stats-Dashboard). Aplikace slouží ke sledování, ukládání a vizualizaci statistik o přenesených datech (placených i neplacených) a výdělcích z vašeho účtu v síti UrNetwork. +Your existing account will be migrated automatically! -Celá aplikace je zabalena do jediného souboru `main_app.py` pro jednoduchost, ale obsahuje robustní funkce, včetně webového instalátoru, autentizace, veřejného a soukromého pohledu, interaktivních grafů, mapy poskytovatelů a podpory pro webhooky. +📖 **Full upgrade guide:** [UPGRADE_GUIDE.md](UPGRADE_GUIDE.md) -> **Poznámka od Autora** -> -> Vím, že asi dostanu hejt za to, že jsem na stavbu a refaktoring použil AI, ale když k tomu mám přístup zadarmo, chci toho co nejvíc využít. Navíc, nic proti mým kamarádům, co umí programovat, ale Gemini mi to často vysvětlí srozumitelněji! (no hate) -> (Tento soubor readme je také vytvořen umělou inteligencí... Prosím, nenávidějte mě, jsem debilní idiot, který je skoro mrtvýmozkově, a to ani nepřeháním.) +--- ------ +### 🐛 Troubleshooting -### ✨ Klíčové Vlastnosti +**Can't login?** +```bash +# Check admin password exists +cat .env | grep ADMIN_PASSWORD - * **🐍 All-in-One Python Skript:** Celá logika aplikace, včetně frontendu, je obsažena v jediném souboru `main_app.py`. - * **🚀 Webový Instalátor:** Při prvním spuštění vás provede jednoduchým nastavením přihlašovacích údajů k UrNetwork API. - * **🔐 Dva Pohledy (Public/Private):** - * **Veřejný Dashboard:** Zobrazuje souhrnné statistiky, celkový objem dat v čase a mapu poskytovatelů po celém světě. Ideální pro rychlý přehled. - * **Soukromý Dashboard:** Po přihlášení získáte přístup k detailním grafům, kompletní historii měření, správě webhooků a dalším administrátorským funkcím. - * **📊 Interaktivní Grafy:** Využívá `Chart.js` pro vizualizaci dat, včetně: - * Celkový objem dat v čase. - * Porovnání placených vs. neplacených dat. - * Graf přírůstků dat mezi jednotlivými měřeními. - * **🗺️ Mapa Poskytovatelů:** Zobrazuje na mapě světa (pomocí `Leaflet.js`) počet aktivních poskytovatelů v jednotlivých zemích. - * **⚙️ Automatický Sběr Dat:** Plánovač (`APScheduler`) automaticky stahuje a ukládá data z API každých 15 minut. - * **🔔 Notifikace přes Webhooky:** Možnost přidat si vlastní webhooky (např. pro Discord) a dostávat notifikace při každé nové synchronizaci dat. - * **🎨 Moderní UI:** - * Stylováno pomocí **Tailwind CSS**. - * Podpora **Světlého/Tmavého režimu** s možností synchronizace s nastavením systému. - * Plně responzivní design. - * **💾 Perzistentní Úložiště:** Využívá `SQLAlchemy` a `SQLite` databázi pro ukládání historických dat. +# If missing, add it +echo "ADMIN_PASSWORD=your_password" >> .env +``` ------ +**Charts not showing text?** +- Make sure you're using `main.py` v2.1+ with white text -### 🚀 Jak začít +**Webhook not working?** +1. Check URL is correct +2. Validate JSON at [jsonlint.com](https://jsonlint.com/) +3. Check logs: `tail -f nohup.out | grep webhook` -#### Požadavky +**Port already in use?** +```python +# Edit last line of main.py +app.run(host="0.0.0.0", port=8080, debug=False) # Change port +``` - * Python 3.x - * Pip (správce balíčků pro Python) +--- -#### Instalace +### 🚀 Production Deployment -1. **Klonujte repozitář:** +**With Gunicorn (recommended):** +```bash +pip install gunicorn +gunicorn --bind 0.0.0.0:90 --workers 4 main:app +``` - ```bash - git clone - cd - ``` +**Systemd Service:** +```ini +[Unit] +Description=UrNetwork Stats Dashboard +After=network.target -2. **Vytvořte a aktivujte virtuální prostředí (doporučeno):** +[Service] +Type=simple +User=your_user +WorkingDirectory=/path/to/urnetwork-stats +ExecStart=/usr/bin/python3 main.py +Restart=always - ```bash - python -m venv venv - # Windows - .venv\Scripts\activate - # macOS / Linux - source venv/bin/activate - ``` +[Install] +WantedBy=multi-user.target +``` -3. **Nainstalujte potřebné balíčky:** - Vytvořte soubor `requirements.txt` s následujícím obsahem: +```bash +sudo systemctl enable urnetwork-stats +sudo systemctl start urnetwork-stats +``` - ```txt - Flask - Flask-SQLAlchemy - Flask-APScheduler - requests - python-dateutil - gunicorn - ``` +--- - A poté je nainstalujte: +### 📊 Screenshots - ```bash - pip install -r requirements.txt - ``` +**Public Dashboard:** +- Combined statistics from all accounts +- Individual charts per account +- World map of providers -4. **Spusťte aplikaci:** +**Private Dashboard:** +- Overview with paid vs unpaid charts +- Account details & leaderboard +- Device management - ```bash - python main_app.py - ``` +**Account Management:** +- Add unlimited accounts +- Toggle on/off +- Custom nicknames - *Poznámka: Pro produkční nasazení je lepší použít WSGI server jako `gunicorn`.* +--- - ```bash - gunicorn --bind 0.0.0.0:90 main_app:app - ``` +### 🙏 Credits -5. **Webová konfigurace:** +- **Original:** [techroy23/UrNetwork-Stats-Dashboard](https://github.com/techroy23/UrNetwork-Stats-Dashboard) +- **v2.0+ Enhanced by:** Claude (Anthropic AI) +- **Design Inspiration:** Cloudflare, Vercel +- **Special Thanks:** Vlastík (mxnticek) for testing - * Otevřete v prohlížeči adresu `http://localhost:90` (nebo IP adresu vašeho serveru). - * Budete přesměrováni na instalační stránku. Zde zadejte své **uživatelské jméno** a **heslo** k účtu UrNetwork. - * Po úspěšném ověření se automaticky vytvoří soubor `.env` s vašimi údaji a vygeneruje se tajný klíč pro session. Aplikace se tímto nainstaluje. +--- ------ +### 📜 License -### 🕹️ Použití +Extends the original UrNetwork Stats Dashboard. Please respect the original author's work. -Po instalaci je aplikace připravena k použití. +--- - * **Veřejný Dashboard (`/`):** Hlavní stránka dostupná komukoliv. Zobrazuje souhrnné statistiky a mapu. Data na této stránce se automaticky aktualizují každou minutu bez nutnosti obnovení stránky. - * **Soukromý Dashboard (`/dashboard`):** - * Pro přístup klikněte na "Owner Dashboard". - * Přihlaste se pomocí stejných údajů, které jste zadali při instalaci. - * Zde naleznete detailní grafy, historii všech měření a můžete spravovat webhooky. - * **Fetch Now:** Manuálně spustí sběr dat z API. - * **Clear All Data:** Smaže veškerá historická data z databáze. - * **Webhook Management:** Přidávejte nebo odebírejte URL pro notifikace. +### 🔮 Roadmap (v3.0?) ------ +- [ ] Dark/Light theme toggle +- [ ] Email notifications +- [ ] Export to CSV/JSON +- [ ] Mobile app (React Native) +- [ ] Advanced analytics +- [ ] Docker container +- [ ] Cost calculator -### 🛠️ Použité Technologie +--- - * **Backend:** [Flask](https://flask.palletsprojects.com/) - * **Databáze:** [Flask-SQLAlchemy](https://flask-sqlalchemy.palletsprojects.com/) (s SQLite) - * **Plánovač úloh:** [Flask-APScheduler](https://github.com/viniciuschiele/flask-apscheduler) - * **Frontend:** [Tailwind CSS](https://tailwindcss.com/), [Chart.js](https://www.chartjs.org/), [Leaflet.js](https://leafletjs.com/) - * **HTTP Požadavky:** [Requests](https://requests.readthedocs.io/en/latest/) +### 📞 Support ------ +- **Issues:** Use GitHub/Forgejo Issues +- **Pull Requests:** Contributions welcome! +- **Documentation:** Help improve docs -### 🙏 Poděkování +--- - * Původní koncept a projekt vytvořil **[techroy23](https://github.com/techroy23/UrNetwork-Stats-Dashboard)**. - * Tato verze byla kompletně refaktorována a rozšířena pomocí AI. \ No newline at end of file +## 🇨🇿 Česká Dokumentace + +**Kompletní český README:** [README_CZ.md](README_CZ.md) + +### Rychlý Start + +```bash +# 1. Klonovat +git clone https://forgejo.plainrock127.xyz/mxnticek/UrNetwork-Stats-Dashboard-remade +cd UrNetwork-Stats-Dashboard-remade + +# 2. Instalace +pip install Flask Flask-SQLAlchemy Flask-APScheduler requests python-dateutil + +# 3. Spustit +python main.py + +# 4. Otevřít prohlížeč +# http://localhost:90 +``` + +### Klíčové Funkce + +- 🎯 **Multi-Account** - Sledujte neomezený počet účtů +- 📊 **Vylepšené Grafy** - Bílý text, tooltip kdekoliv +- 🔐 **Oddělená Auth** - Admin heslo nezávislé na UrNetwork +- ⚡ **Denní Čištění** - Auto-mazání dat starších 7 dní +- 📱 **Mobilní** - Touch-friendly rozhraní + +### Webhook Příklady + +**Discord - Jednoduchý:** +```json +{ + "content": "📊 **${account}**: ${total_gb} GB" +} +``` + +**Discord - Bohatý:** +```json +{ + "embeds": [{ + "title": "⚡ ${account} - Nová Data!", + "color": 3901635, + "fields": [ + {"name": "💰 Placená", "value": "`${paid_gb} GB`", "inline": true}, + {"name": "📡 Neplacená", "value": "`${unpaid_gb} GB`", "inline": true} + ] + }] +} +``` + +**Telegram:** +```json +{ + "chat_id": "VAŠE_CHAT_ID", + "text": "📊 *${account}*\n💾 Celkem: ${total_gb} GB", + "parse_mode": "Markdown" +} +``` + +📖 **Kompletní návod:** [WEBHOOK_GUIDE.md](WEBHOOK_GUIDE.md) + +--- + +**Version:** v2.1.1 | **Last Updated:** November 21, 2024 + +**Made with ❤️ and 🤖 AI** diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f639651 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Flask==3.0.0 +Flask-SQLAlchemy==3.1.1 +Flask-APScheduler==1.13.1 +requests==2.31.0 +python-dateutil==2.8.2 +gunicorn==21.2.0