kanban / app.py
github-action
deploy: 2026-03-21 02:15
609e433
Raw
History Blame Contribute Delete
33.2 kB
import streamlit as st
import pandas as pd
from streamlit_cookies_controller import CookieController
from datetime import datetime, timedelta
import hashlib, random, string, requests, json, time, uuid
from streamlit.components.v1 import html as _html
from tenacity import retry, wait_exponential, stop_after_attempt, retry_if_exception
from config import (
BREVO_API_KEY, EMAIL_FROM_NAME, EMAIL_FROM_ADDRESS,
GS_SPREADSHEET, open_spreadsheet,
)
st.set_page_config(
page_title="TaskFlow | Login",
page_icon="πŸ”",
layout="wide",
initial_sidebar_state="collapsed"
)
# ── COOKIE SESSION ────────────────────────────────────────────────────
_cc = CookieController()
COOKIE_NAME = "pmja_session"
SESSION_EXPIRY_HOURS = 8
def save_session(user_id, username, expiry_hours=SESSION_EXPIRY_HOURS):
expiry = datetime.now() + timedelta(hours=expiry_hours)
_cc.set(COOKIE_NAME, {
"user_id": str(user_id),
"username": username,
"expiry": expiry.isoformat(),
"token": str(uuid.uuid4()),
})
st.session_state.logged_in = True
st.session_state.session_uid = str(user_id)
st.session_state.session_usr = username
st.session_state.session_exp = expiry
def load_session():
# 1) session_state ainda ativo (mesma aba sem recarregar)
if st.session_state.get("logged_in") and st.session_state.get("session_exp"):
if datetime.now() < st.session_state.session_exp:
return {
"user_id": st.session_state.session_uid,
"username": st.session_state.session_usr,
}
clear_session()
return None
# 2) cookie do browser (apΓ³s F5 ou nova aba no mesmo browser)
try:
c = _cc.get(COOKIE_NAME)
if not c:
return None
expiry = datetime.fromisoformat(c["expiry"])
if datetime.now() >= expiry:
_cc.remove(COOKIE_NAME)
return None
st.session_state.logged_in = True
st.session_state.session_uid = c["user_id"]
st.session_state.session_usr = c["username"]
st.session_state.session_exp = expiry
return {"user_id": c["user_id"], "username": c["username"]}
except Exception:
return None
def clear_session():
try:
_cc.remove(COOKIE_NAME)
except Exception:
pass
st.session_state.update(
logged_in=False, user_data=None,
session_uid=None, session_usr=None, session_exp=None,
)
# ── BREVO β€” validado tarde (apΓ³s CSS) ────────────────────────────────
# ── DB (gspread) ──────────────────────────────────────────────────────
def _is_quota_or_server_error(exc: BaseException) -> bool:
msg = str(exc)
return any(code in msg for code in ("429", "500", "503", "RESOURCE_EXHAUSTED", "quota"))
@retry(
retry=retry_if_exception(_is_quota_or_server_error),
wait=wait_exponential(multiplier=2, min=5, max=60),
stop=stop_after_attempt(5),
reraise=True,
)
def _read_worksheet(worksheet: str) -> pd.DataFrame:
"""Le uma worksheet com retry automatico em caso de cota esgotada."""
try:
ws = open_spreadsheet().worksheet(worksheet)
data = ws.get_all_records()
return pd.DataFrame(data) if data else pd.DataFrame()
except Exception as exc:
if _is_quota_or_server_error(exc):
raise # deixa o tenacity fazer retry
return pd.DataFrame()
def _serialize(v):
"""Converte valores para string sem o sufixo .0 em floats inteiros."""
if v is None or v == "" or (isinstance(v, float) and pd.isna(v)):
return ""
try:
f = float(v)
if f == int(f) and not str(v).startswith("="):
return str(int(f))
except (ValueError, TypeError):
pass
return str(v)
@retry(
retry=retry_if_exception(_is_quota_or_server_error),
wait=wait_exponential(multiplier=2, min=5, max=60),
stop=stop_after_attempt(5),
reraise=True,
)
def _update_worksheet(worksheet: str, df: pd.DataFrame) -> None:
"""Sobrescreve a worksheet com retry automatico em caso de cota esgotada."""
if df.empty:
return
ws = open_spreadsheet().worksheet(worksheet)
rows = [[_serialize(v) for v in row] for row in df.fillna("").values.tolist()]
ws.clear()
ws.update('A1', [df.columns.tolist()] + rows, value_input_option='USER_ENTERED')
def hash_password(p):
return hashlib.sha256(p.encode()).hexdigest()
def generate_code():
return "".join(random.choices(string.digits, k=6))
def send_verification_email(email, code, username):
try:
body = (
"<div style='font-family:sans-serif;max-width:420px;margin:0 auto;"
"padding:32px;background:#f8fafc;border-radius:12px;'>"
"<p style='font-size:10px;color:#94a3b8;margin:0 0 4px;'>PMJA Β· Scrum</p>"
"<h2 style='color:#0f172a;font-size:18px;font-weight:600;margin:0 0 18px;'>"
"CΓ³digo de verificaΓ§Γ£o</h2>"
"<div style='background:#fff;border:1px solid #e2e8f0;border-radius:10px;"
"padding:22px;text-align:center;margin-bottom:18px;'>"
f"<span style='font-size:30px;font-weight:700;letter-spacing:.22em;color:#0075be;'>{code}</span>"
"</div>"
"<p style='color:#94a3b8;font-size:12px;margin:0;'>VΓ‘lido por 5 minutos.</p></div>"
)
r = requests.post(
"https://api.brevo.com/v3/smtp/email",
headers={"api-key": BREVO_API_KEY, "content-type": "application/json"},
data=json.dumps({
"sender": {"name": EMAIL_FROM_NAME, "email": EMAIL_FROM_ADDRESS},
"to": [{"email": email}],
"subject": "CΓ³digo – PMJA",
"htmlContent": body,
}),
)
return r.status_code in [200, 201]
except Exception:
return False
COLS = [
"id", "username", "email", "password", "full_name", "created_at",
"last_login", "email_verified", "verification_code", "code_expiry", "image_url",
]
def init_users_sheet():
try:
df = _read_worksheet("users_auth")
if not df.empty:
for c in COLS:
if c not in df.columns:
df[c] = ""
df["email_verified"] = (
df["email_verified"].astype(str).str.strip().str.lower().replace("nan", "false")
)
for c in ["verification_code", "code_expiry", "image_url"]:
df[c] = df[c].fillna("").astype(str).replace("nan", "").replace("None", "")
return df
except Exception:
return pd.DataFrame(columns=COLS)
def get_user_by_id(uid):
df = init_users_sheet()
uid = str(uid)
u = df[df["id"].astype(str) == uid]
return u.iloc[0].to_dict() if not u.empty else None
def login_user(username, password):
df = init_users_sheet()
if df.empty:
return False, "Nenhum usuΓ‘rio cadastrado"
u = df[df["username"] == username]
if u.empty:
return False, "UsuΓ‘rio nΓ£o encontrado"
if u.iloc[0]["password"] != hash_password(password):
return False, "Senha incorreta"
if str(u.iloc[0]["email_verified"]).strip().lower() not in ["true", "1", "yes", "1.0"]:
return False, "EMAIL_NOT_VERIFIED"
return True, u.iloc[0].to_dict()
def register_user(username, email, password, full_name, image_url=""):
df = init_users_sheet()
if not df.empty and username in df["username"].values:
return False, "UsuΓ‘rio jΓ‘ existe", None
if not df.empty and email in df["email"].values:
return False, "Email jΓ‘ cadastrado", None
code = generate_code()
expiry = (datetime.now() + timedelta(minutes=5)).strftime("%d/%m/%Y %H:%M:%S")
new_id = 1 if df.empty else int(df["id"].max()) + 1
row = {
"id": str(new_id), "username": str(username), "email": str(email),
"password": hash_password(password), "full_name": str(full_name),
"created_at": datetime.now().strftime("%d/%m/%Y %H:%M:%S"), "last_login": "",
"email_verified": "false", "verification_code": str(code),
"code_expiry": str(expiry),
"image_url": str(image_url).strip() if image_url else "",
}
_update_worksheet(
"users_auth",
pd.concat([df, pd.DataFrame([row])], ignore_index=True),
)
send_verification_email(email, code, username)
return True, "Cadastro realizado!", code
def resend_verification_code(username):
df = init_users_sheet()
u = df[df["username"] == username]
if u.empty:
return False, "UsuΓ‘rio nΓ£o encontrado"
code = generate_code()
expiry = (datetime.now() + timedelta(minutes=5)).strftime("%d/%m/%Y %H:%M:%S")
email = u.iloc[0]["email"]
df2 = _read_worksheet("users_auth")
idx = df2[df2["username"] == username].index[0]
df2.loc[idx, "verification_code"] = str(code)
df2.loc[idx, "code_expiry"] = str(expiry)
_update_worksheet("users_auth", df2)
send_verification_email(email, code, username)
return True, "CΓ³digo reenviado!"
def verify_email_code(username, code):
df = init_users_sheet()
u = df[df["username"] == username]
if u.empty:
return False, "UsuΓ‘rio nΓ£o encontrado"
stored = str(u.iloc[0]["verification_code"]).strip().replace(" ", "").replace(".0", "")
inp = str(code).strip().replace(" ", "")
if stored != inp and stored.lstrip("0") != inp.lstrip("0"):
return False, "CΓ³digo incorreto"
df2 = _read_worksheet("users_auth")
idx = df2[df2["username"] == username].index[0]
df2.loc[idx, "email_verified"] = "true"
df2.loc[idx, "verification_code"] = ""
df2.loc[idx, "code_expiry"] = ""
_update_worksheet("users_auth", df2)
time.sleep(1)
return True, "Email verificado!"
# ── session state ─────────────────────────────────────────────────────
for k, v in [
("logged_in", False),
("user_data", None),
("page", "login"),
("temp_username", None),
("msg", ""),
("msg_type", ""),
("_action", None),
("session_uid", None),
("session_usr", None),
("session_exp", None),
]:
if k not in st.session_state:
st.session_state[k] = v
# ── verifica sessΓ£o ativa ─────────────────────────────────────────────
# O CookieController precisa de um rerun para ler os cookies do browser.
# Na primeira renderizacao ele ainda nao os carregou, entao bloqueamos
# o render do formulario ate o cookie estar disponivel (ou confirmado ausente).
_cookie_raw = _cc.get(COOKIE_NAME) # None = ainda nao lido OR nao existe
if not st.session_state.get("_cookies_ready"):
# Se nao ha cookie, nao precisa esperar β€” marca pronto e continua
if _cookie_raw is None:
st.session_state["_cookies_ready"] = True
else:
# Ha cookie mas o estado interno ainda nao foi populado β€” carrega direto
st.session_state["_cookies_ready"] = True
# Carrega sessao direto aqui antes do st.stop()
try:
expiry = datetime.fromisoformat(_cookie_raw["expiry"])
if datetime.now() < expiry:
st.session_state.logged_in = True
st.session_state.session_uid = _cookie_raw["user_id"]
st.session_state.session_usr = _cookie_raw["username"]
st.session_state.session_exp = expiry
ud = get_user_by_id(_cookie_raw["user_id"])
if ud:
st.session_state.user_data = ud
st.switch_page("pages/tasks.py")
st.stop()
else:
_cc.remove(COOKIE_NAME)
except Exception:
pass
if not st.session_state.logged_in:
s = load_session()
if s:
ud = get_user_by_id(s["user_id"])
if ud:
st.session_state.logged_in = True
st.session_state.user_data = ud
st.switch_page("pages/tasks.py")
st.stop() # nao renderiza nada enquanto o redirect acontece
# ── process pending action ────────────────────────────────────────────
action = st.session_state.pop("_action", None) if "_action" in st.session_state else None
if action == "go_register":
st.session_state.page = "register"
st.session_state.msg = ""
elif action == "go_login":
st.session_state.page = "login"
st.session_state.msg = ""
elif action == "resend":
ok, msg = resend_verification_code(st.session_state.temp_username)
st.session_state.msg = msg
st.session_state.msg_type = "success" if ok else "error"
elif action == "logout":
clear_session()
st.session_state.page = "login"
elif action == "extend":
user = st.session_state.user_data
if user:
save_session(user["id"], user["username"], SESSION_EXPIRY_HOURS)
st.session_state.msg = "SessΓ£o estendida por 2 horas"
st.session_state.msg_type = "success"
# ── CSS ───────────────────────────────────────────────────────────────
_CSS = """\
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&display=swap');
#MainMenu,footer,header,.stDeployButton,
[data-testid="stToolbar"],
[data-testid="stDecoration"],
[data-testid="stStatusWidget"]{display:none!important}
*,*::before,*::after{box-sizing:border-box!important;font-family:'DM Sans',-apple-system,sans-serif!important}
html,body{height:100%;margin:0;padding:0}
section[data-testid="stMain"],
section[data-testid="stMain"]>div{
min-height:100vh!important;padding:0!important;background:#f0f4f8!important
}
div.block-container{
min-height:100vh!important;max-width:100%!important;padding:32px 12px!important;
margin:0!important;background:#f0f4f8!important;
display:flex!important;align-items:center!important;justify-content:center!important
}
div.block-container>div[data-testid="stVerticalBlock"]{
width:100%!important;display:flex!important;flex-direction:column!important;
align-items:center!important;justify-content:center!important;gap:0!important
}
div[data-testid="stHorizontalBlock"]{
width:100%!important;display:flex!important;
justify-content:center!important;align-items:flex-start!important;gap:0!important
}
div[data-testid="stHorizontalBlock"]>div[data-testid="stColumn"]:first-child,
div[data-testid="stHorizontalBlock"]>div[data-testid="stColumn"]:last-child{
flex:1 1 0!important;min-width:0!important;padding:0!important
}
div[data-testid="stHorizontalBlock"]>div[data-testid="stColumn"]:nth-child(2){
flex:0 0 min(460px,calc(100vw - 24px))!important;
width:min(460px,calc(100vw - 24px))!important;
max-width:min(460px,calc(100vw - 24px))!important;
min-width:0!important;padding:0!important;background:#ffffff!important;
border-radius:20px!important;overflow:hidden!important;
box-shadow:0 0 0 1px rgba(0,0,0,.06),0 4px 6px rgba(0,0,0,.05),0 24px 60px rgba(0,0,0,.14)!important
}
div[data-testid="stHorizontalBlock"]>div[data-testid="stColumn"]:nth-child(2)>div[data-testid="stVerticalBlock"]{gap:0!important}
div[data-testid="stHorizontalBlock"]>div[data-testid="stColumn"]:nth-child(2)>div[data-testid="stVerticalBlock"]>div{padding-left:28px!important;padding-right:28px!important}
div[data-testid="stHorizontalBlock"]>div[data-testid="stColumn"]:nth-child(2)>div[data-testid="stVerticalBlock"]>div:first-child{padding-left:0!important;padding-right:0!important}
div[data-testid="stHorizontalBlock"]>div[data-testid="stColumn"]:nth-child(2)>div[data-testid="stVerticalBlock"]>div:nth-child(2){padding-top:22px!important;padding-bottom:4px!important}
div[data-testid="stHorizontalBlock"]>div[data-testid="stColumn"]:nth-child(2)>div[data-testid="stVerticalBlock"]>div:last-child{padding-bottom:28px!important}
div[data-testid="stForm"]{border:none!important;padding:0!important;background:transparent!important;border-radius:0!important}
div[data-testid="stForm"]>div>div[data-testid="stVerticalBlock"]{gap:0!important;padding:0!important}
div[data-testid="stTextInput"] label{display:block!important;margin-bottom:6px!important}
div[data-testid="stTextInput"] label p{font-size:12px!important;font-weight:600!important;color:#64748b!important;letter-spacing:.025em!important;margin:0!important;line-height:1!important}
div[data-testid="stTextInput"]>div>div>input{height:44px!important;background:#f8fafc!important;border:1.5px solid #e2e8f0!important;border-radius:12px!important;padding:0 14px!important;font-size:14px!important;color:#0f172a!important;width:100%!important;outline:none!important;box-shadow:none!important;transition:border-color .15s,box-shadow .15s,background .15s!important}
div[data-testid="stTextInput"]>div>div>input::placeholder{color:#c8d3dd!important;font-size:13.5px!important}
div[data-testid="stTextInput"]>div>div>input:focus{border-color:#0075be!important;background:#ffffff!important;box-shadow:0 0 0 3px rgba(0,117,190,.13)!important}
div[data-testid="InputInstructions"]{display:none!important}
div[data-testid="stTextInput"]{margin-top:14px!important;margin-bottom:0!important}
div[data-testid="stFormSubmitButton"]{margin-top:22px!important}
div[data-testid="stButton"]>button,div[data-testid="stFormSubmitButton"]>button{font-size:14px!important;font-weight:600!important;height:46px!important;border-radius:12px!important;width:100%!important;cursor:pointer!important;transition:all .18s!important;letter-spacing:.01em!important}
div[data-testid="stFormSubmitButton"]>button[kind="primary"],div[data-testid="stButton"]>button[kind="primary"]{background:#0075be!important;color:#ffffff!important;border:none!important;box-shadow:0 1px 2px rgba(0,0,0,.08),0 4px 14px rgba(0,117,190,.28)!important}
div[data-testid="stFormSubmitButton"]>button[kind="primary"]:hover,div[data-testid="stButton"]>button[kind="primary"]:hover{background:#005fa0!important;box-shadow:0 2px 4px rgba(0,0,0,.1),0 8px 20px rgba(0,117,190,.36)!important;transform:translateY(-1px)!important}
div[data-testid="stFormSubmitButton"]>button[kind="secondary"],div[data-testid="stButton"]>button[kind="secondary"]{background:#ffffff!important;color:#64748b!important;border:1.5px solid #e2e8f0!important;box-shadow:none!important}
div[data-testid="stFormSubmitButton"]>button[kind="secondary"]:hover,div[data-testid="stButton"]>button[kind="secondary"]:hover{background:#f8fafc!important;border-color:#0075be!important;color:#0075be!important}
div[data-testid="stForm"]>div>div[data-testid="stVerticalBlock"]>div[data-testid="stHorizontalBlock"]{gap:12px!important;width:100%!important;margin-top:14px!important;padding:0!important;justify-content:flex-start!important;align-items:flex-start!important}
div[data-testid="stForm"]>div>div[data-testid="stVerticalBlock"]>div[data-testid="stHorizontalBlock"]>div[data-testid="stColumn"]{flex:1 1 0!important;min-width:0!important;padding:0!important}
div[data-testid="stForm"]>div>div[data-testid="stVerticalBlock"]>div[data-testid="stHorizontalBlock"] div[data-testid="stTextInput"]{margin-top:0!important}
div[data-testid="stButton"]{margin:0!important}
div[data-testid="stAlert"]{font-size:13px!important;border-radius:12px!important;margin-bottom:16px!important;margin-top:4px!important}
</style>
"""
_html(_CSS, height=0)
# ── Brevo: validado apΓ³s CSS para erro ficar estilizado ───────────────
if not BREVO_API_KEY or not EMAIL_FROM_ADDRESS:
st.error("Configure BREVO_API_KEY e EMAIL_FROM_ADDRESS no .env")
st.stop()
# ── page state ────────────────────────────────────────────────────────
pg = "dashboard" if st.session_state.logged_in else st.session_state.page
user = st.session_state.user_data
PHOTOS = {
"login": "https://drudu6g9smo13.cloudfront.net/wp-content/uploads/2023/08/UHE-Jaguara.jpg",
"register": "https://s2.glbimg.com/e844-OclDbLw-PWuboUy_wtbhiQ=/512x320/smart/e.glbimg.com/og/ed/f/original/2021/12/01/hidreletrica_jaguara_-_rifaina_sp_SGhwNiF.jpg",
"verify": "https://upload.wikimedia.org/wikipedia/commons/9/99/Usina_Hidrel%C3%A9trica_de_Jaguara_%28Rifaina-SP_%284478616135%29.jpg",
"dashboard": "https://drudu6g9smo13.cloudfront.net/wp-content/uploads/2023/08/UHE-Jaguara.jpg",
}
TITLES = {
"login": ("Bem-vindo de volta", "ACESSAR CONTA"),
"register": ("Criar conta", "DADOS DE CADASTRO"),
"verify": ("Verificar email", "CΓ“DIGO DE VERIFICAÇÃO"),
"dashboard": (
("OlΓ‘, " + user["full_name"].split()[0]) if user else "Painel",
"PAINEL",
),
}
banner_title, form_label = TITLES.get(pg, TITLES["login"])
photo = PHOTOS.get(pg, PHOTOS["login"])
def flush():
m, t = st.session_state.msg, st.session_state.msg_type
st.session_state.msg = ""
st.session_state.msg_type = ""
if not m:
return
{"error": st.error, "success": st.success}.get(t, st.info)(m)
def sp(px):
st.markdown(f"<div style='height:{px}px;line-height:0;font-size:0;'></div>", unsafe_allow_html=True)
def divider():
st.markdown(
"<div style='display:flex;align-items:center;gap:12px;margin:18px 0 14px;'>"
"<div style='flex:1;height:1px;background:#e2e8f0;'></div>"
"<span style='font-size:11.5px;color:#94a3b8;font-weight:500;'>ou</span>"
"<div style='flex:1;height:1px;background:#e2e8f0;'></div></div>",
unsafe_allow_html=True,
)
# ═════════════════════════════════════════════════════════════════════
# LAYOUT
# ═════════════════════════════════════════════════════════════════════
_, card, _ = st.columns([1, 1.4, 1])
with card:
# BANNER
st.markdown(
f"<div style='position:relative;width:100%;height:196px;overflow:hidden;'>"
f"<img src='{photo}' style='position:absolute;inset:0;width:100%;height:100%;"
"object-fit:cover;object-position:center 40%;filter:brightness(.55) saturate(1.2);'>"
"<div style='position:absolute;inset:0;background:linear-gradient(180deg,transparent 20%,rgba(15,23,42,.72) 100%);'></div>"
"<div style='position:absolute;bottom:0;left:0;right:0;padding:0 28px 22px;'>"
"<div style='font-size:9px;font-weight:700;letter-spacing:.22em;color:rgba(255,255,255,.42);text-transform:uppercase;margin-bottom:7px;line-height:1;'>PMJA Β· UHE Jaguara Β· Rifaina SP</div>"
f"<div style='font-size:23px;font-weight:700;color:#fff;line-height:1.2;letter-spacing:-.2px;text-shadow:0 2px 10px rgba(0,0,0,.3);'>{banner_title}</div>"
"</div></div>",
unsafe_allow_html=True,
)
# SECTION LABEL
st.markdown(
f"<div style='font-size:9px;font-weight:700;letter-spacing:.22em;text-transform:uppercase;color:#0075be;line-height:1;'>{form_label}</div>",
unsafe_allow_html=True,
)
# ══════════════════════════════════════════════════════════════
# LOGIN
# ══════════════════════════════════════════════════════════════
if pg == "login":
flush()
sp(14)
with st.form("f_login", border=False):
st.text_input("UsuΓ‘rio", placeholder="nome_usuario", key="li_u")
st.text_input("Senha", placeholder="β€’β€’β€’β€’β€’β€’β€’β€’", type="password", key="li_p")
sub = st.form_submit_button("Entrar β†’", type="primary", use_container_width=True)
if sub:
u = st.session_state.li_u.strip()
p = st.session_state.li_p
if not (u and p):
st.session_state.msg = "Preencha todos os campos"
st.session_state.msg_type = "error"
st.rerun()
else:
ok, result = login_user(u, p)
if ok:
st.session_state.logged_in = True
st.session_state.user_data = result
save_session(result["id"], u, SESSION_EXPIRY_HOURS)
st.switch_page("pages/tasks.py")
elif result == "EMAIL_NOT_VERIFIED":
resend_verification_code(u)
st.session_state.temp_username = u
st.session_state.page = "verify"
st.session_state.msg = "πŸ“§ CΓ³digo reenviado para seu email"
st.session_state.msg_type = "info"
st.rerun()
else:
st.session_state.msg = result
st.session_state.msg_type = "error"
st.rerun()
divider()
if st.button("Criar nova conta β†’", use_container_width=True, key="go_reg"):
st.session_state.page = "register"
st.session_state.msg = ""
st.rerun()
sp(4)
# ══════════════════════════════════════════════════════════════
# REGISTER
# ══════════════════════════════════════════════════════════════
elif pg == "register":
flush()
sp(14)
with st.form("f_register", border=False):
st.text_input("Nome completo", placeholder="Seu nome completo", key="rg_n")
st.text_input("Email", placeholder="seu@email.com", key="rg_e")
c1, c2 = st.columns(2)
with c1: st.text_input("UsuΓ‘rio", placeholder="nome_usuario", key="rg_u")
with c2: st.text_input("URL da foto", placeholder="https://...", key="rg_i")
c3, c4 = st.columns(2)
with c3: st.text_input("Senha", placeholder="MΓ­n. 6 caracteres", type="password", key="rg_p")
with c4: st.text_input("Confirmar senha", placeholder="Repita a senha", type="password", key="rg_p2")
sub = st.form_submit_button("Cadastrar β†’", type="primary", use_container_width=True)
if sub:
fn = st.session_state.rg_n.strip()
em = st.session_state.rg_e.strip()
us = st.session_state.rg_u.strip()
img = st.session_state.rg_i.strip()
pw = st.session_state.rg_p
pw2 = st.session_state.rg_p2
if not all([fn, em, us, pw, pw2]):
st.session_state.msg = "Preencha todos os campos"
st.session_state.msg_type = "error"
elif len(pw) < 6:
st.session_state.msg = "Senha mΓ­nimo 6 caracteres"
st.session_state.msg_type = "error"
elif pw != pw2:
st.session_state.msg = "Senhas nΓ£o coincidem"
st.session_state.msg_type = "error"
else:
ok, msg, _ = register_user(us, em, pw, fn, img)
if ok:
st.session_state.temp_username = us
st.session_state.page = "verify"
st.session_state.msg = "Conta criada! Verifique seu email."
st.session_state.msg_type = "success"
else:
st.session_state.msg = msg
st.session_state.msg_type = "error"
st.rerun()
divider()
if st.button("← JΓ‘ tenho conta", use_container_width=True, key="go_log"):
st.session_state.page = "login"
st.session_state.msg = ""
st.rerun()
sp(4)
# ══════════════════════════════════════════════════════════════
# VERIFY
# ══════════════════════════════════════════════════════════════
elif pg == "verify":
flush()
sp(14)
st.markdown(
"<div style='display:flex;align-items:center;gap:10px;"
"background:#eff6ff;border:1px solid #bfdbfe;border-radius:12px;"
"padding:12px 15px;margin-bottom:16px;'>"
"<span style='font-size:18px;flex-shrink:0;'>πŸ“§</span>"
"<span style='font-size:13px;color:#1d4ed8;font-weight:500;line-height:1.4;'>"
"CΓ³digo enviado por email. VΓ‘lido por 5 minutos.</span></div>",
unsafe_allow_html=True,
)
with st.form("f_verify", border=False):
st.text_input("CΓ³digo de 6 dΓ­gitos", placeholder="000000", max_chars=6, key="vf_c")
sp(8)
cv1, cv2 = st.columns(2)
with cv1: confirm = st.form_submit_button("Confirmar", type="primary", use_container_width=True)
with cv2: go_back = st.form_submit_button("← Voltar", use_container_width=True)
if confirm:
ok, msg = verify_email_code(st.session_state.temp_username, st.session_state.vf_c)
st.session_state.page = "login" if ok else "verify"
st.session_state.msg = msg
st.session_state.msg_type = "success" if ok else "error"
st.rerun()
if go_back:
st.session_state.page = "login"
st.rerun()
sp(8)
if st.button("Reenviar cΓ³digo por email", use_container_width=True, key="resend"):
ok, msg = resend_verification_code(st.session_state.temp_username)
st.session_state.msg = msg
st.session_state.msg_type = "success" if ok else "error"
st.rerun()
sp(4)
# ══════════════════════════════════════════════════════════════
# DASHBOARD
# ══════════════════════════════════════════════════════════════
else:
flush()
uname = user["full_name"] if user else ""
uemail = user["email"] if user else ""
initial = uname[0].upper() if uname else "?"
sp(14)
st.markdown(
"<div style='display:flex;align-items:center;gap:14px;"
"background:#f8fafc;border:1.5px solid #e2e8f0;border-radius:14px;padding:14px 16px;margin-bottom:18px;'>"
"<div style='width:44px;height:44px;border-radius:50%;flex-shrink:0;"
"background:linear-gradient(135deg,#0075be,#38bdf8);display:flex;align-items:center;justify-content:center;"
f"color:#fff;font-weight:700;font-size:18px;box-shadow:0 2px 8px rgba(0,117,190,.3);'>{initial}</div>"
f"<div><div style='font-size:14px;font-weight:600;color:#0f172a;'>{uname}</div>"
f"<div style='font-size:12px;color:#64748b;margin-top:3px;'>{uemail}</div></div></div>",
unsafe_allow_html=True,
)
if st.button("πŸ“‹ Acessar Tarefas β†’", type="primary", use_container_width=True, key="tasks"):
st.switch_page("pages/tasks.py")
sp(10)
d1, d2 = st.columns(2)
with d1:
if st.button("πŸ”„ Estender SessΓ£o", use_container_width=True, key="extend"):
save_session(user["id"], user["username"], SESSION_EXPIRY_HOURS)
st.session_state.msg = "SessΓ£o estendida por 2 horas"
st.session_state.msg_type = "success"
st.rerun()
with d2:
if st.button("Sair", use_container_width=True, key="logout"):
clear_session()
st.session_state.page = "login"
st.rerun()
sp(4)
# FOOTER
st.markdown(
"<div style='text-align:center;padding-top:12px;font-size:10px;color:#94a3b8;letter-spacing:.1em;'>"
"PMJA Β· UHE Jaguara Β· Rifaina SP</div>",
unsafe_allow_html=True,
)