lazier

personal summarizer
Log | Files | Refs | README

commit 9aee2918523d3b3c972a83289f6154b0ba94e9c2
parent fb15684f92b5c5e34cd9f9bcc1c2d40500e65dc7
Author: Pablo Murad <pablo@pablomurad.com>
Date:   Tue, 14 Apr 2026 13:31:40 -0300

remodelação

Diffstat:
Mlazier/api/auth_routes.py | 43++++++++++++++++++++++++++++++++++---------
Mlazier/web/templates/index.html | 24+++++++++++++++++-------
Mlazier/web/templates/login.html | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
3 files changed, 198 insertions(+), 71 deletions(-)

diff --git a/lazier/api/auth_routes.py b/lazier/api/auth_routes.py @@ -14,6 +14,12 @@ router = APIRouter() # Diretório de templates templates_dir = Path(__file__).parent.parent / "web" / "templates" +# Marcador único em login.html para injeção de erro (evita replace frágil em </form>) +_LOGIN_ERROR_PLACEHOLDER = '<div id="login-error" class="error" role="alert" hidden></div>' +_LOGIN_ERROR_VISIBLE = ( + '<div id="login-error" class="error" role="alert">Usuário ou senha incorretos.</div>' +) + @router.get("/login") async def login_page(): @@ -92,27 +98,41 @@ async def login_page(): } .error { color: #d32f2f; - margin-top: 10px; + margin-top: 12px; + padding: 10px; text-align: center; font-size: 14px; + background: rgba(211, 47, 47, 0.08); + border-radius: 8px; + word-break: break-word; } </style> </head> <body> <div class="login-container"> - <h1>🎧 Lazier</h1> + <h1>Lazier · acesso privado</h1> <form method="POST" action="/login"> <div class="form-group"> <label for="username">Usuário</label> - <input type="text" id="username" name="username" required autofocus> + <input type="text" id="username" name="username" required autofocus autocomplete="username"> </div> <div class="form-group"> <label for="password">Senha</label> - <input type="password" id="password" name="password" required> + <input type="password" id="password" name="password" required autocomplete="current-password"> </div> <button type="submit">Entrar</button> + <div id="login-error" class="error" role="alert" hidden></div> </form> </div> + <script> + (function () { + var p = new URLSearchParams(window.location.search); + if (p.get('error') === 'invalid') { + var b = document.getElementById('login-error'); + if (b) { b.textContent = 'Usuário ou senha incorretos.'; b.removeAttribute('hidden'); } + } + })(); + </script> </body> </html> """ @@ -128,15 +148,20 @@ async def login( """Processa login e cria sessão""" # Verifica credenciais if not verify_password(username, password): - # Retorna página de login com erro login_file = templates_dir / "login.html" if login_file.exists(): with open(login_file, 'r', encoding='utf-8') as f: html = f.read() - html = html.replace('</form>', '<div class="error">Usuário ou senha incorretos</div></form>') - return HTMLResponse(content=html, status_code=401) - - # Fallback: redirect com erro + if _LOGIN_ERROR_PLACEHOLDER in html: + html = html.replace( + _LOGIN_ERROR_PLACEHOLDER, + _LOGIN_ERROR_VISIBLE, + 1, + ) + else: + # Template antigo sem placeholder: redireciona com query + return RedirectResponse(url="/login?error=invalid", status_code=303) + return HTMLResponse(content=html, status_code=200) return RedirectResponse(url="/login?error=invalid", status_code=303) # Cria sessão diff --git a/lazier/web/templates/index.html b/lazier/web/templates/index.html @@ -11,13 +11,13 @@ <style> :root { --bg:#f5f1ea; --card:rgba(255,255,255,.9); --ink:#223437; --muted:#66787b; --line:rgba(34,52,55,.12); --accent:#2f7a71; --accent-soft:rgba(47,122,113,.1); --warn:#b85f49; } * { box-sizing:border-box; margin:0; padding:0; } - body { font-family:'Manrope',sans-serif; color:var(--ink); background:radial-gradient(circle at top left, rgba(47,122,113,.08), transparent 26%), linear-gradient(180deg,#faf7f2 0%,#f2ede6 100%); min-height:100vh; padding:20px; } + body { font-family:'Manrope',sans-serif; color:var(--ink); background:radial-gradient(circle at top left, rgba(47,122,113,.08), transparent 26%), linear-gradient(180deg,#faf7f2 0%,#f2ede6 100%); min-height:100vh; min-height:100dvh; padding:20px; } a { color:inherit; text-decoration:none; } button,input,select { font:inherit; } .shell { max-width:1180px; margin:0 auto; } .topbar,.card,.job,.modal-card { background:var(--card); border:1px solid rgba(255,255,255,.9); box-shadow:0 18px 42px rgba(40,56,60,.08); backdrop-filter:blur(14px); } .topbar { border-radius:999px; padding:16px 20px; display:flex; justify-content:space-between; gap:16px; align-items:center; margin-bottom:20px; flex-wrap:wrap; } - .brand { display:flex; gap:12px; align-items:center; } + .brand { display:flex; gap:12px; align-items:center; flex:1; min-width:0; } .mark { width:42px; height:42px; border-radius:14px; display:grid; place-items:center; background:linear-gradient(135deg, rgba(47,122,113,.12), rgba(198,125,69,.12)); } .brand h1,.title { font-family:'Fraunces',serif; letter-spacing:-.03em; } .brand p,.subtle,.hero p,.field label,.meta,.empty,.error { color:var(--muted); } @@ -41,7 +41,8 @@ .upload { padding:24px; border-radius:24px; border:1.5px dashed rgba(47,122,113,.28); background:linear-gradient(180deg, rgba(223,240,236,.55), rgba(255,255,255,.75)); cursor:pointer; } .upload.dragover { transform:translateY(-1px); } .file-list { display:grid; gap:10px; } - .file { display:grid; grid-template-columns:1fr auto auto; gap:10px; align-items:center; padding:12px 14px; border-radius:16px; background:#fff; border:1px solid var(--line); } + .file { display:grid; grid-template-columns:1fr auto auto; gap:10px; align-items:center; padding:12px 14px; border-radius:16px; background:#fff; border:1px solid var(--line); min-width:0; } + .file > div:first-child { min-width:0; overflow-wrap:anywhere; word-break:break-word; } .remove { width:34px; height:34px; border-radius:999px; border:0; background:rgba(184,95,73,.1); color:var(--warn); cursor:pointer; } .mode-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:10px; } .mode { border:1px solid var(--line); border-radius:20px; padding:16px; cursor:pointer; background:rgba(255,255,255,.7); } @@ -49,14 +50,15 @@ .mode input { display:none; } .mode strong { display:block; margin-bottom:6px; } .row { display:grid; grid-template-columns:1fr 220px; gap:12px; align-items:end; } - .btn { border:0; border-radius:16px; padding:14px 16px; background:linear-gradient(135deg,var(--accent),#225b55); color:#fff; font-weight:700; cursor:pointer; } + .btn { border:0; border-radius:16px; padding:14px 16px; background:linear-gradient(135deg,var(--accent),#225b55); color:#fff; font-weight:700; cursor:pointer; touch-action:manipulation; min-height:44px; } .btn:disabled { opacity:.65; cursor:not-allowed; } .section { margin-top:20px; } .header { display:flex; justify-content:space-between; gap:14px; align-items:end; margin-bottom:14px; } .list { display:grid; gap:12px; } .job { border-radius:22px; padding:20px; } - .job-head { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; } - .job-title { font-weight:700; line-height:1.45; } + .job-head { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; min-width:0; } + .job-head > div:first-child { min-width:0; flex:1; } + .job-title { font-weight:700; line-height:1.45; overflow-wrap:anywhere; word-break:break-word; } .chips,.actions,.filters { display:flex; gap:8px; flex-wrap:wrap; } .chip,.status { border-radius:999px; padding:8px 12px; font-size:.82rem; } .chip { background:rgba(34,52,55,.06); color:var(--muted); } @@ -79,7 +81,15 @@ .preview h1,.preview h2,.preview h3 { font-family:'Fraunces',serif; margin:16px 0 8px; } .preview hr { border:0; border-top:1px solid var(--line); margin:18px 0; } @media (max-width:980px) { .hero,.row { grid-template-columns:1fr; } .notes { grid-template-columns:1fr; } } - @media (max-width:640px) { body { padding:14px; } .mode-grid { grid-template-columns:1fr; } .job-head,.header { flex-direction:column; align-items:flex-start; } } + @media (max-width:640px) { + body { padding:14px; } + .mode-grid { grid-template-columns:1fr; } + .job-head,.header { flex-direction:column; align-items:flex-start; } + .topbar { border-radius:22px; } + .nav { width:100%; justify-content:flex-start; } + .nav a,.nav button { min-height:40px; display:inline-flex; align-items:center; } + .field input,.field select { font-size:16px; } + } </style> </head> <body> diff --git a/lazier/web/templates/login.html b/lazier/web/templates/login.html @@ -8,63 +8,155 @@ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@500;700&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet"> <style> - :root { --bg:#f5f1ea; --card:rgba(255,255,255,.9); --ink:#223437; --muted:#66787b; --line:rgba(34,52,55,.12); --accent:#2f7a71; --warn:#b85f49; } - * { box-sizing:border-box; margin:0; padding:0; } - body { min-height:100vh; display:grid; place-items:center; padding:20px; font-family:'Manrope',sans-serif; color:var(--ink); background:radial-gradient(circle at top left, rgba(47,122,113,.08), transparent 26%), linear-gradient(180deg,#faf7f2 0%,#f2ede6 100%); } - .shell { width:min(980px,100%); display:grid; grid-template-columns:1.05fr .95fr; gap:18px; } - .hero,.card { background:var(--card); border:1px solid rgba(255,255,255,.9); box-shadow:0 18px 42px rgba(40,56,60,.08); backdrop-filter:blur(14px); border-radius:28px; } - .hero,.card { padding:28px; } - .hero { display:flex; flex-direction:column; justify-content:space-between; min-height:430px; } - .eyebrow { display:inline-block; background:rgba(47,122,113,.1); color:var(--accent); padding:8px 12px; border-radius:999px; font-size:.85rem; font-weight:700; } - h1,h2 { font-family:'Fraunces',serif; letter-spacing:-.04em; line-height:1.05; } - h1 { font-size:clamp(2.2rem,4vw,3.4rem); margin:18px 0; } - p,.point span,label,.footer { color:var(--muted); } - .point { border:1px solid var(--line); border-radius:18px; padding:14px; background:rgba(255,255,255,.6); margin-top:10px; } - .point strong { display:block; margin-bottom:6px; color:var(--ink); } - .card h2 { font-size:2rem; margin-bottom:8px; } - .card p { margin-bottom:22px; } - .field { display:flex; flex-direction:column; gap:8px; margin-bottom:16px; } - label { font-size:.88rem; font-weight:700; } - input { width:100%; padding:14px 16px; border-radius:16px; border:1px solid var(--line); background:rgba(255,255,255,.95); color:var(--ink); } - button { width:100%; border:0; border-radius:16px; padding:14px 16px; background:linear-gradient(135deg,var(--accent),#225b55); color:#fff; font-weight:700; cursor:pointer; margin-top:6px; } - .error { margin-top:14px; color:var(--warn); background:rgba(184,95,73,.1); border:1px solid rgba(184,95,73,.16); border-radius:14px; padding:12px 14px; } - .footer { margin-top:16px; text-align:center; font-size:.84rem; } - @media (max-width:880px) { .shell { grid-template-columns:1fr; } .hero,.card { min-height:auto; padding:22px; } } + :root { + --bg: #f5f1ea; + --card: rgba(255, 255, 255, 0.92); + --ink: #223437; + --muted: #66787b; + --line: rgba(34, 52, 55, 0.12); + --accent: #2f7a71; + --accent-soft: rgba(47, 122, 113, 0.12); + --warn: #b85f49; + } + * { box-sizing: border-box; margin: 0; padding: 0; } + body { + min-height: 100vh; + min-height: 100dvh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: clamp(16px, 4vw, 28px); + font-family: 'Manrope', sans-serif; + color: var(--ink); + background: radial-gradient(circle at top left, rgba(47, 122, 113, 0.08), transparent 26%), + linear-gradient(180deg, #faf7f2 0%, #f2ede6 100%); + } + .card { + width: 100%; + max-width: 420px; + background: var(--card); + border: 1px solid rgba(255, 255, 255, 0.95); + border-radius: 24px; + padding: clamp(22px, 5vw, 32px); + box-shadow: 0 18px 42px rgba(40, 56, 60, 0.08); + backdrop-filter: blur(14px); + } + .eyebrow { + display: inline-block; + background: var(--accent-soft); + color: var(--accent); + padding: 6px 12px; + border-radius: 999px; + font-size: 0.8rem; + font-weight: 700; + margin-bottom: 12px; + } + h1 { + font-family: 'Fraunces', serif; + font-size: clamp(1.45rem, 4vw, 1.75rem); + letter-spacing: -0.03em; + line-height: 1.2; + margin-bottom: 8px; + } + .sub { + color: var(--muted); + font-size: 0.9rem; + line-height: 1.5; + margin-bottom: 22px; + } + .field { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; + } + label { + font-size: 0.88rem; + font-weight: 700; + color: var(--ink); + } + input { + width: 100%; + padding: 14px 16px; + border-radius: 14px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.95); + color: var(--ink); + font-size: 16px; + } + input:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } + button[type="submit"] { + width: 100%; + border: 0; + border-radius: 14px; + padding: 14px 16px; + min-height: 44px; + margin-top: 8px; + background: linear-gradient(135deg, var(--accent), #225b55); + color: #fff; + font-weight: 700; + font-size: 1rem; + cursor: pointer; + touch-action: manipulation; + } + button[type="submit"]:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } + .error { + margin-top: 14px; + padding: 12px 14px; + border-radius: 12px; + color: var(--warn); + background: rgba(184, 95, 73, 0.1); + border: 1px solid rgba(184, 95, 73, 0.2); + font-size: 0.9rem; + line-height: 1.45; + word-break: break-word; + } + .footer { + margin-top: 24px; + text-align: center; + font-size: 0.82rem; + color: var(--muted); + } </style> </head> <body> - <div> - <div class="shell"> - <section class="hero"> - <div> - <div class="eyebrow">Lazier</div> - <h1>Entrada simples, saída organizada e sempre em português.</h1> - <p>O Lazier recebe mídias, páginas, textos e PDFs, persiste o histórico e mantém o fluxo mais claro: transcrever ou resumir.</p> - </div> - <div> - <div class="point"><strong>Transcrição ou sumário</strong><span>Um objetivo por vez, com resultado mais consistente.</span></div> - <div class="point"><strong>Histórico persistente</strong><span>Seus jobs continuam visíveis após reiniciar o servidor.</span></div> - <div class="point"><strong>Arquivos mais fáceis de achar</strong><span>As saídas ficam agrupadas por data e job.</span></div> - </div> - </section> - <section class="card"> - <h2>Entrar</h2> - <p>Use sua conta para acessar a WebGUI.</p> - <form method="POST" action="/login"> - <div class="field"> - <label for="username">Usuário</label> - <input type="text" id="username" name="username" required autofocus autocomplete="username" placeholder="Digite seu usuário"> - </div> - <div class="field"> - <label for="password">Senha</label> - <input type="password" id="password" name="password" required autocomplete="current-password" placeholder="Digite sua senha"> - </div> - <button type="submit">Entrar</button> - </form> - </section> - </div> - <div class="footer">Desenvolvido por Pablo Murad · <span id="currentYear"></span></div> - </div> - <script>document.getElementById('currentYear').textContent = new Date().getFullYear();</script> + <main class="card"> + <div class="eyebrow">Lazier</div> + <h1>Acesso privado</h1> + <p class="sub">Uso pessoal. Entre com o usuário e a senha configurados no servidor.</p> + <form method="POST" action="/login"> + <div class="field"> + <label for="username">Usuário</label> + <input type="text" id="username" name="username" required autofocus autocomplete="username" placeholder="Usuário"> + </div> + <div class="field"> + <label for="password">Senha</label> + <input type="password" id="password" name="password" required autocomplete="current-password" placeholder="Senha"> + </div> + <button type="submit">Entrar</button> + <div id="login-error" class="error" role="alert" hidden></div> + </form> + </main> + <p class="footer">Lazier · <span id="currentYear"></span></p> + <script> + document.getElementById('currentYear').textContent = new Date().getFullYear(); + (function () { + var params = new URLSearchParams(window.location.search); + var err = params.get('error'); + var box = document.getElementById('login-error'); + if (!box) return; + if (err === 'invalid') { + box.textContent = 'Usuário ou senha incorretos.'; + box.removeAttribute('hidden'); + } + })(); + </script> </body> </html>