commit 9aee2918523d3b3c972a83289f6154b0ba94e9c2
parent fb15684f92b5c5e34cd9f9bcc1c2d40500e65dc7
Author: Pablo Murad <pablo@pablomurad.com>
Date: Tue, 14 Apr 2026 13:31:40 -0300
remodelação
Diffstat:
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>