CONFIGURAÇÕES 2

This commit is contained in:
Flatlogic Bot 2026-02-18 01:25:04 +00:00
parent 9f27edfd4b
commit 36fc77f98e
12 changed files with 334 additions and 133 deletions

View File

@ -0,0 +1,49 @@
# Generated by Django 5.2.7 on 2026-02-18 01:18
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AdminAccess',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('private_key', models.CharField(default=uuid.uuid4, max_length=255, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Lottery',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(choices=[('mega_sena', 'Mega-Sena'), ('quina', 'Quina'), ('dupla_sena', 'Dupla Sena'), ('lotomania', 'Lotomania'), ('lotofacil', 'Lotofácil')], max_length=50, unique=True)),
('min_number', models.IntegerField(default=1)),
('max_number', models.IntegerField()),
('numbers_to_draw', models.IntegerField()),
('annulled_numbers', models.TextField(blank=True, default='')),
],
),
migrations.CreateModel(
name='DrawResult',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('draw_number', models.IntegerField()),
('draw_date', models.DateField()),
('numbers', models.CharField(max_length=255)),
('lottery', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='draws', to='core.lottery')),
],
options={
'ordering': ['-draw_date'],
'unique_together': {('lottery', 'draw_number')},
},
),
]

View File

@ -1,3 +1,44 @@
from django.db import models
import uuid
# Create your models here.
class AdminAccess(models.Model):
"""Armazena a chave privada única para acesso ao painel."""
private_key = models.CharField(max_length=255, unique=True, default=uuid.uuid4)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Key created on {self.created_at}"
class Lottery(models.Model):
"""Configurações específicas de cada tipo de loteria."""
LOTTERY_TYPES = [
('mega_sena', 'Mega-Sena'),
('quina', 'Quina'),
('dupla_sena', 'Dupla Sena'),
('lotomania', 'Lotomania'),
('lotofacil', 'Lotofácil'),
]
name = models.CharField(max_length=50, choices=LOTTERY_TYPES, unique=True)
min_number = models.IntegerField(default=1)
max_number = models.IntegerField()
numbers_to_draw = models.IntegerField()
# Lista de números anulados manualmente (armazenado como string separada por vírgula)
annulled_numbers = models.TextField(default="", blank=True)
def __str__(self):
return self.get_name_display()
class DrawResult(models.Model):
"""Resultados reais dos sorteios da Caixa."""
lottery = models.ForeignKey(Lottery, on_delete=models.CASCADE, related_name='draws')
draw_number = models.IntegerField()
draw_date = models.DateField()
numbers = models.CharField(max_length=255) # Ex: "05,12,34,45,56,59"
class Meta:
unique_together = ('lottery', 'draw_number')
ordering = ['-draw_date']
def __str__(self):
return f"{self.lottery.name} - Concurso {self.draw_number}"

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5 pt-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Painel Administrativo</h2>
<a href="{% url 'admin_logout' %}" class="btn btn-ghost">Sair</a>
</div>
<div class="row g-4">
{% for lottery in loterias %}
<div class="col-md-4">
<div class="card-soft p-4 h-100">
<h3>{{ lottery.get_name_display }}</h3>
<p class="text-muted">Números: 1 a {{ lottery.max_number }}</p>
<hr>
<a href="{% url 'edit_lottery' lottery.id %}" class="btn btn-brand w-100">Editar Números</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5 pt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card-glass p-5 text-center">
<h2 class="mb-4">Acesso Administrativo</h2>
<p class="text-muted">Insira sua Private Key para acessar as configurações.</p>
<form method="post">
{% csrf_token %}
<div class="mb-4">
<input type="password" name="private_key" class="form-control form-control-lg text-center" placeholder="Chave Privada" required>
</div>
<button type="submit" class="btn btn-brand btn-lg w-100">Acessar Painel</button>
</form>
{% if messages %}
{% for message in messages %}
<div class="alert alert-danger mt-3">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,70 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5 pt-5">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'admin_dashboard' %}">Painel</a></li>
<li class="breadcrumb-item active">{{ lottery.get_name_display }}</li>
</ol>
</nav>
<div class="card-glass p-4 mb-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Editor: {{ lottery.get_name_display }}</h2>
<p class="text-muted">Selecione os números para <strong>ANULAR</strong> no próximo sorteio.</p>
</div>
<form method="post">
{% csrf_token %}
<div class="lottery-grid mb-4">
{% for n in numbers %}
<div class="form-check number-toggle">
<input class="form-check-input d-none" type="checkbox" name="numbers" value="{{ n }}" id="num_{{ n }}" {% if n in annulled %}checked{% endif %}>
<label class="number-ball {% if n in annulled %}annulled{% endif %}" for="num_{{ n }}">
{{ n|stringformat:"02d" }}
</label>
</div>
{% endfor %}
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-brand btn-lg">Salvar Configurações</button>
</div>
</form>
</div>
</div>
<style>
.lottery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 15px;
background: rgba(255,255,255,0.05);
padding: 20px;
border-radius: 15px;
}
.number-ball {
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #0b1f2a;
color: white;
cursor: pointer;
transition: all 0.3s;
border: 2px solid rgba(255,255,255,0.1);
font-weight: bold;
}
.number-ball:hover {
background: #0f766e;
transform: scale(1.1);
}
.number-toggle input:checked + .number-ball {
background: #dc3545 !important; /* Vermelho real para Anulado */
border-color: #fff;
box-shadow: 0 0 15px rgba(220, 53, 69, 0.5);
}
</style>
{% endblock %}

View File

@ -18,7 +18,11 @@
<li class="nav-item"><a class="nav-link" href="#simulador">Simulador</a></li>
<li class="nav-item"><a class="nav-link" href="#jogos">Jogos</a></li>
<li class="nav-item"><a class="nav-link" href="#como-funciona">Como funciona</a></li>
<li class="nav-item"><a class="nav-link" href="/admin/">Admin</a></li>
{% if is_admin %}
<li class="nav-item"><a class="nav-link text-warning" href="{% url 'admin_dashboard' %}">Painel Admin</a></li>
{% else %}
<li class="nav-item"><a class="nav-link" href="{% url 'admin_login' %}">Acesso Admin</a></li>
{% endif %}
<li class="nav-item">
<a class="btn btn-brand" href="#simulador">Gerar jogos</a>
</li>
@ -126,19 +130,22 @@
</div>
<div class="number-groups">
<div>
<div class="group-title">Quentes no recorte</div>
<div class="group-title text-success">Probabilidades Quentes (Verde)</div>
<div class="badge-grid">
{% for number in result.hot_numbers %}
<span class="badge badge-hot">{{ number }}</span>
<span class="badge" style="background-color: #198754; font-size: 1.1rem;">{{ number|stringformat:"02d" }}</span>
{% endfor %}
</div>
</div>
<div>
<div class="group-title">Frias no recorte</div>
<div class="mt-3">
<div class="group-title text-danger">Números Anulados (Vermelho)</div>
<div class="badge-grid">
{% for number in result.cold_numbers %}
<span class="badge badge-cold">{{ number }}</span>
{% for number in result.annulled_numbers %}
<span class="badge" style="background-color: #dc3545; font-size: 1.1rem;">{{ number|stringformat:"02d" }}</span>
{% endfor %}
{% if not result.annulled_numbers %}
<small class="text-muted opacity-50">Nenhum numero anulado pelo admin.</small>
{% endif %}
</div>
</div>
</div>

View File

@ -1,7 +1,10 @@
from django.urls import path
from .views import home
from . import views
urlpatterns = [
path("", home, name="home"),
path('', views.home, name='home'),
path('admin-loto/', views.admin_login, name='admin_login'),
path('admin-loto/logout/', views.admin_logout, name='admin_logout'),
path('admin-loto/dashboard/', views.admin_dashboard, name='admin_dashboard'),
path('admin-loto/edit/<int:lottery_id>/', views.edit_lottery, name='edit_lottery'),
]

View File

@ -5,121 +5,95 @@ import random
from collections import Counter
from django import get_version as django_version
from django.shortcuts import render
from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone
from django.contrib import messages
from .forms import LotterySimulatorForm
from .models import Lottery, DrawResult, AdminAccess
def check_admin(request):
"""Verifica se a chave privada na sessão é válida."""
key = request.session.get('admin_key')
return AdminAccess.objects.filter(private_key=key).exists()
LOTTERY_CONFIGS = {
"mega_sena": {
"label": "Mega-Sena",
"range_max": 60,
"picks": 6,
"sample_draws": 15,
"seed": 1982,
"tagline": "6 dezenas entre 60",
},
"quina": {
"label": "Quina",
"range_max": 80,
"picks": 5,
"sample_draws": 15,
"seed": 1971,
"tagline": "5 dezenas entre 80",
},
"dupla_sena": {
"label": "Dupla Sena",
"range_max": 50,
"picks": 6,
"sample_draws": 15,
"seed": 1990,
"tagline": "6 dezenas entre 50",
},
"lotomania": {
"label": "Lotomania",
"range_max": 100,
"picks": 50,
"sample_draws": 12,
"seed": 1999,
"tagline": "50 dezenas entre 100",
},
"lotofacil": {
"label": "Lotofacil",
"range_max": 25,
"picks": 15,
"sample_draws": 20,
"seed": 1994,
"tagline": "15 dezenas entre 25",
},
}
def admin_login(request):
"""Tela de login simples para a Chave Privada."""
if request.method == 'POST':
key = request.POST.get('private_key')
if AdminAccess.objects.filter(private_key=key).exists():
request.session['admin_key'] = key
return redirect('admin_dashboard')
else:
messages.error(request, "Chave Privada Inválida!")
return render(request, 'core/admin_login.html')
def admin_logout(request):
request.session.flush()
return redirect('home')
def _generate_sample_draws(config):
rng = random.Random(config["seed"])
draws = []
population = list(range(1, config["range_max"] + 1))
for _ in range(config["sample_draws"]):
draws.append(sorted(rng.sample(population, config["picks"])))
return draws
def admin_dashboard(request):
"""Painel principal do administrador."""
if not check_admin(request):
return redirect('admin_login')
loterias = Lottery.objects.all()
return render(request, 'core/admin_dashboard.html', {'loterias': loterias})
def edit_lottery(request, lottery_id):
"""Editor específico para cada jogo (anular números)."""
if not check_admin(request):
return redirect('admin_login')
lottery = get_object_or_404(Lottery, id=lottery_id)
# Ajuste para Lotomania (0-99 se necessário, mas mantendo 1-100 por padrão)
numbers = range(1, lottery.max_number + 1)
annulled = [int(n) for n in lottery.annulled_numbers.split(',') if n]
def _weighted_unique_sample(numbers, weights, picks, rng):
available = list(zip(numbers, weights))
selection = []
for _ in range(picks):
total_weight = sum(weight for _, weight in available)
if total_weight <= 0:
choice = rng.choice(available)
selection.append(choice[0])
available.remove(choice)
continue
pick = rng.uniform(0, total_weight)
cumulative = 0
for index, (number, weight) in enumerate(available):
cumulative += weight
if cumulative >= pick:
selection.append(number)
del available[index]
break
return selection
if request.method == 'POST':
selected_numbers = request.POST.getlist('numbers')
lottery.annulled_numbers = ",".join(selected_numbers)
lottery.save()
messages.success(request, f"Configurações da {lottery.get_name_display()} salvas!")
return redirect('admin_dashboard')
return render(request, 'core/edit_lottery.html', {
'lottery': lottery,
'numbers': numbers,
'annulled': annulled
})
def _format_odds(total_combinations):
if total_combinations >= 1_000_000_000_000:
return f"1 em {total_combinations:.2e}"
return f"1 em {total_combinations:,}".replace(",", ".")
def _format_percent(odds):
percent = 100 / odds
if percent < 0.000001:
return "<0,000001%"
return f"{percent:.6f}%".replace(".", ",")
def home(request):
"""Render the landing screen with lottery simulator and insights."""
host_name = request.get_host().lower()
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
now = timezone.now()
lottery_choices = [
(key, config["label"]) for key, config in LOTTERY_CONFIGS.items()
]
loterias_db = Lottery.objects.all()
lottery_choices = [(l.name, l.get_name_display()) for l in loterias_db]
lottery_cards = []
for key, config in LOTTERY_CONFIGS.items():
total_combinations = math.comb(config["range_max"], config["picks"])
lottery_cards.append(
{
"key": key,
"label": config["label"],
"tagline": config["tagline"],
"range_max": config["range_max"],
"picks": config["picks"],
"odds": _format_odds(total_combinations),
}
)
for l in loterias_db:
total_combinations = math.comb(l.max_number, l.numbers_to_draw)
lottery_cards.append({
"key": l.name,
"label": l.get_name_display(),
"tagline": f"{l.numbers_to_draw} dezenas entre {l.max_number}",
"range_max": l.max_number,
"picks": l.numbers_to_draw,
"odds": _format_odds(total_combinations),
})
form = LotterySimulatorForm(
request.POST or None,
@ -131,64 +105,75 @@ def home(request):
lottery_key = form.cleaned_data["lottery_type"]
draws_to_consider = form.cleaned_data["draws_to_consider"]
games_to_generate = form.cleaned_data["games_to_generate"]
config = LOTTERY_CONFIGS[lottery_key]
draws = _generate_sample_draws(config)
draws_to_consider = min(draws_to_consider, len(draws))
recent_draws = draws[-draws_to_consider:]
frequency = Counter(
number for draw in recent_draws for number in draw
)
numbers = list(range(1, config["range_max"] + 1))
lottery_obj = Lottery.objects.get(name=lottery_key)
annulled = [int(n) for n in lottery_obj.annulled_numbers.split(',') if n]
# Busca sorteios reais no banco
draws_db = DrawResult.objects.filter(lottery=lottery_obj)[:draws_to_consider]
draw_lists = []
for d in draws_db:
draw_lists.append([int(n) for n in d.numbers.split(',')])
# Se não houver sorteios reais, usa aleatórios para manter a app funcionando
if not draw_lists:
rng_mock = random.Random(42)
population = list(range(1, lottery_obj.max_number + 1))
for _ in range(draws_to_consider):
draw_lists.append(rng_mock.sample(population, lottery_obj.numbers_to_draw))
frequency = Counter(number for draw in draw_lists for number in draw)
numbers = [n for n in range(1, lottery_obj.max_number + 1) if n not in annulled]
# Números Quentes (Verde) e Frios (Vermelho)
# Quentes: Maior frequência e não anulados
hot_candidates = frequency.most_common(15)
hot_numbers = [n for n, c in hot_candidates if n not in annulled][:8]
# Pesos para geração baseados na frequência
weights = [frequency.get(number, 0) + 1 for number in numbers]
rng = random.Random(f"{lottery_key}-{draws_to_consider}-{games_to_generate}")
suggestions = []
for _ in range(games_to_generate):
suggestion = _weighted_unique_sample(
numbers, weights, config["picks"], rng
)
suggestions.append(sorted(suggestion))
if len(numbers) >= lottery_obj.numbers_to_draw:
# Simulação ponderada simples
indices = list(range(len(numbers)))
ws = [frequency.get(numbers[i], 0) + 1 for i in indices]
selected_indices = random.choices(indices, weights=ws, k=lottery_obj.numbers_to_draw)
# Garante que não repete números no mesmo jogo
game = []
temp_indices = indices.copy()
for _p in range(lottery_obj.numbers_to_draw):
ws_temp = [frequency.get(numbers[i], 0) + 1 for i in temp_indices]
idx = random.choices(range(len(temp_indices)), weights=ws_temp, k=1)[0]
game.append(numbers[temp_indices[idx]])
del temp_indices[idx]
suggestions.append(sorted(game))
total_combinations = math.comb(config["range_max"], config["picks"])
hot_numbers = [
number for number, _ in frequency.most_common(8)
]
cold_numbers = [
number
for number, _ in sorted(
frequency.items(), key=lambda item: item[1]
)
]
missing_numbers = [
number for number in numbers if number not in frequency
]
cold_numbers = (missing_numbers + cold_numbers)[:8]
total_combinations = math.comb(lottery_obj.max_number, lottery_obj.numbers_to_draw)
result = {
"lottery": config["label"],
"draws_used": draws_to_consider,
"lottery": lottery_obj.get_name_display(),
"draws_used": len(draw_lists),
"total_combinations": f"{total_combinations:,}".replace(",", "."),
"odds": _format_odds(total_combinations),
"percent": _format_percent(total_combinations),
"suggestions": suggestions,
"hot_numbers": hot_numbers,
"cold_numbers": cold_numbers,
"annulled_numbers": annulled,
}
context = {
"project_name": "LotoPulse",
"project_description": (
"Analise loterias brasileiras, gere jogos e acompanhe "
"probabilidades com base nos sorteios mais recentes."
),
"project_description": "Análise matemática e editor de probabilidades para Loterias Caixa.",
"agent_brand": agent_brand,
"django_version": django_version(),
"python_version": platform.python_version(),
"current_time": now,
"host_name": host_name,
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
"deployment_timestamp": now.strftime("%Y%m%d%H%M%S"),
"form": form,
"result": result,
"lottery_cards": lottery_cards,
"is_admin": check_admin(request),
}
return render(request, "core/index.html", context)