Spaces:
Running
Running
Login UI + Neon-ready DATABASE_URL (2026-07-30 06:41 UTC) (part 2)
Browse files- README.md +3 -1
- frontend/index.html +2 -1
- frontend/login.html +131 -0
- serve.py +3 -0
README.md
CHANGED
|
@@ -14,6 +14,7 @@ Single Space process on port **7860**:
|
|
| 14 |
|
| 15 |
| Path | Content |
|
| 16 |
|------|---------|
|
|
|
|
| 17 |
| `/` | AIMONEYFLOW client portal (Vite, fast) |
|
| 18 |
| `/clients/` | Existing client HTML dossiers (Kliendibaas) |
|
| 19 |
| `/dashboard.html` | Client dashboard |
|
|
@@ -33,4 +34,5 @@ Single Space process on port **7860**:
|
|
| 33 |
- `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `DEEPSEEK_API_KEY`, …
|
| 34 |
- `ALLOWED_ORIGINS` — CORS list
|
| 35 |
|
| 36 |
-
<!-- last_local_sync: 2026-07-30
|
|
|
|
|
|
| 14 |
|
| 15 |
| Path | Content |
|
| 16 |
|------|---------|
|
| 17 |
+
| `/login.html` | Login / register UI |
|
| 18 |
| `/` | AIMONEYFLOW client portal (Vite, fast) |
|
| 19 |
| `/clients/` | Existing client HTML dossiers (Kliendibaas) |
|
| 20 |
| `/dashboard.html` | Client dashboard |
|
|
|
|
| 34 |
- `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `DEEPSEEK_API_KEY`, …
|
| 35 |
- `ALLOWED_ORIGINS` — CORS list
|
| 36 |
|
| 37 |
+
<!-- last_local_sync: 2026-07-30 06:41 UTC login+neon-ready -->
|
| 38 |
+
|
frontend/index.html
CHANGED
|
@@ -13,5 +13,6 @@
|
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
<div id="root"></div>
|
| 16 |
-
</body>
|
| 17 |
</html>
|
|
|
|
|
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
<div id="root"></div>
|
| 16 |
+
<a href="/login.html" style="position:fixed;top:12px;right:12px;z-index:9999;background:#059669;color:#fff;padding:10px 14px;border-radius:8px;text-decoration:none;font-family:system-ui;font-weight:600">Login</a></body>
|
| 17 |
</html>
|
| 18 |
+
|
frontend/login.html
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="et">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>ATOM · Login</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root { --green:#059669; --bg:#f8fafc; --card:#fff; --text:#0f172a; --muted:#64748b; --err:#dc3545; }
|
| 9 |
+
* { box-sizing:border-box; }
|
| 10 |
+
body { margin:0; min-height:100vh; font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
| 11 |
+
background:linear-gradient(145deg,#ecfdf5 0%,#f0f9ff 50%,#f8fafc 100%); color:var(--text);
|
| 12 |
+
display:flex; align-items:center; justify-content:center; padding:1rem; }
|
| 13 |
+
.card { width:100%; max-width:420px; background:var(--card); border-radius:16px; padding:2rem;
|
| 14 |
+
box-shadow:0 20px 50px rgba(15,23,42,.08); border:1px solid #e2e8f0; }
|
| 15 |
+
h1 { margin:0 0 .25rem; font-size:1.75rem; color:var(--green); }
|
| 16 |
+
p.sub { margin:0 0 1.5rem; color:var(--muted); }
|
| 17 |
+
label { display:block; font-size:.875rem; font-weight:600; margin:.75rem 0 .35rem; }
|
| 18 |
+
input { width:100%; padding:.7rem .85rem; border:1px solid #cbd5e1; border-radius:10px; font-size:1rem; }
|
| 19 |
+
input:focus { outline:2px solid #6ee7b7; border-color:var(--green); }
|
| 20 |
+
button { width:100%; margin-top:1.25rem; padding:.85rem; border:0; border-radius:10px;
|
| 21 |
+
background:var(--green); color:#fff; font-weight:700; font-size:1rem; cursor:pointer; min-height:44px; }
|
| 22 |
+
button:disabled { opacity:.6; cursor:not-allowed; }
|
| 23 |
+
button.secondary { background:transparent; color:var(--green); border:1px solid var(--green); margin-top:.75rem; }
|
| 24 |
+
.err { background:#fef2f2; color:var(--err); border:1px solid #fecaca; padding:.75rem; border-radius:10px;
|
| 25 |
+
margin-bottom:1rem; font-size:.9rem; display:none; }
|
| 26 |
+
.ok { background:#ecfdf5; color:#065f46; border:1px solid #a7f3d0; padding:.75rem; border-radius:10px;
|
| 27 |
+
margin-bottom:1rem; font-size:.9rem; display:none; }
|
| 28 |
+
.row { display:grid; grid-template-columns:1fr 1fr; gap:.75rem; }
|
| 29 |
+
.hint { margin-top:1rem; font-size:.8rem; color:var(--muted); text-align:center; }
|
| 30 |
+
a { color:var(--green); }
|
| 31 |
+
</style>
|
| 32 |
+
</head>
|
| 33 |
+
<body>
|
| 34 |
+
<div class="card">
|
| 35 |
+
<h1>ATOM</h1>
|
| 36 |
+
<p class="sub" id="subtitle">Logi sisse oma kontoga</p>
|
| 37 |
+
<div class="err" id="err" role="alert"></div>
|
| 38 |
+
<div class="ok" id="ok" role="status"></div>
|
| 39 |
+
<form id="form" novalidate>
|
| 40 |
+
<div id="names" class="row" style="display:none">
|
| 41 |
+
<div>
|
| 42 |
+
<label for="first_name">Eesnimi</label>
|
| 43 |
+
<input id="first_name" name="first_name" autocomplete="given-name" />
|
| 44 |
+
</div>
|
| 45 |
+
<div>
|
| 46 |
+
<label for="last_name">Perekonnanimi</label>
|
| 47 |
+
<input id="last_name" name="last_name" autocomplete="family-name" />
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
<label for="email">Email</label>
|
| 51 |
+
<input id="email" name="email" type="email" required autocomplete="username" placeholder="sina@email.ee" />
|
| 52 |
+
<label for="password">Parool</label>
|
| 53 |
+
<input id="password" name="password" type="password" required autocomplete="current-password" minlength="6" />
|
| 54 |
+
<button type="submit" id="submit">Logi sisse</button>
|
| 55 |
+
<button type="button" class="secondary" id="toggle">Loo uus konto</button>
|
| 56 |
+
</form>
|
| 57 |
+
<p class="hint">API: <a href="/docs">/docs</a> · Health: <a href="/api/healthz">/api/healthz</a> · <a href="/">Avaleht</a></p>
|
| 58 |
+
</div>
|
| 59 |
+
<script>
|
| 60 |
+
const API = ''; // same origin on HF Space
|
| 61 |
+
let mode = 'login'; // or register
|
| 62 |
+
const err = document.getElementById('err');
|
| 63 |
+
const ok = document.getElementById('ok');
|
| 64 |
+
const names = document.getElementById('names');
|
| 65 |
+
const subtitle = document.getElementById('subtitle');
|
| 66 |
+
const submit = document.getElementById('submit');
|
| 67 |
+
const toggle = document.getElementById('toggle');
|
| 68 |
+
|
| 69 |
+
function showErr(msg) { err.style.display='block'; ok.style.display='none'; err.textContent=msg; }
|
| 70 |
+
function showOk(msg) { ok.style.display='block'; err.style.display='none'; ok.textContent=msg; }
|
| 71 |
+
function clearMsg() { err.style.display='none'; ok.style.display='none'; }
|
| 72 |
+
|
| 73 |
+
function formatDetail(data) {
|
| 74 |
+
if (!data) return 'Viga';
|
| 75 |
+
const d = data.detail ?? data.message ?? data.error;
|
| 76 |
+
if (typeof d === 'string') return d;
|
| 77 |
+
if (Array.isArray(d)) return d.map(x => x.msg || JSON.stringify(x)).join('; ');
|
| 78 |
+
return JSON.stringify(d);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
toggle.onclick = () => {
|
| 82 |
+
mode = mode === 'login' ? 'register' : 'login';
|
| 83 |
+
clearMsg();
|
| 84 |
+
names.style.display = mode === 'register' ? 'grid' : 'none';
|
| 85 |
+
subtitle.textContent = mode === 'login' ? 'Logi sisse oma kontoga' : 'Loo uus ATOM konto';
|
| 86 |
+
submit.textContent = mode === 'login' ? 'Logi sisse' : 'Registreeri';
|
| 87 |
+
toggle.textContent = mode === 'login' ? 'Loo uus konto' : 'Tagasi sisselogimisele';
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
document.getElementById('form').onsubmit = async (e) => {
|
| 91 |
+
e.preventDefault();
|
| 92 |
+
clearMsg();
|
| 93 |
+
submit.disabled = true;
|
| 94 |
+
const email = document.getElementById('email').value.trim();
|
| 95 |
+
const password = document.getElementById('password').value;
|
| 96 |
+
try {
|
| 97 |
+
let res, body;
|
| 98 |
+
if (mode === 'login') {
|
| 99 |
+
res = await fetch(API + '/api/auth/login', {
|
| 100 |
+
method: 'POST',
|
| 101 |
+
headers: { 'Content-Type': 'application/json' },
|
| 102 |
+
body: JSON.stringify({ username: email, password })
|
| 103 |
+
});
|
| 104 |
+
} else {
|
| 105 |
+
body = {
|
| 106 |
+
email, password,
|
| 107 |
+
first_name: document.getElementById('first_name').value.trim() || 'User',
|
| 108 |
+
last_name: document.getElementById('last_name').value.trim() || 'Atom'
|
| 109 |
+
};
|
| 110 |
+
res = await fetch(API + '/api/auth/register', {
|
| 111 |
+
method: 'POST',
|
| 112 |
+
headers: { 'Content-Type': 'application/json' },
|
| 113 |
+
body: JSON.stringify(body)
|
| 114 |
+
});
|
| 115 |
+
}
|
| 116 |
+
const data = await res.json().catch(() => ({}));
|
| 117 |
+
if (!res.ok) throw new Error(formatDetail(data) || ('HTTP ' + res.status));
|
| 118 |
+
if (!data.access_token) throw new Error('Token puudub vastuses');
|
| 119 |
+
localStorage.setItem('token', data.access_token);
|
| 120 |
+
localStorage.setItem('auth_token', data.access_token);
|
| 121 |
+
showOk('Sisselogimine õnnestus — suunan edasi…');
|
| 122 |
+
setTimeout(() => { location.href = '/'; }, 600);
|
| 123 |
+
} catch (ex) {
|
| 124 |
+
showErr(ex.message || String(ex));
|
| 125 |
+
} finally {
|
| 126 |
+
submit.disabled = false;
|
| 127 |
+
}
|
| 128 |
+
};
|
| 129 |
+
</script>
|
| 130 |
+
</body>
|
| 131 |
+
</html>
|
serve.py
CHANGED
|
@@ -28,6 +28,8 @@ os.environ.setdefault(
|
|
| 28 |
"ALLOWED_ORIGINS",
|
| 29 |
"*,https://techprotrade-annator-atom.hf.space,http://localhost:7860",
|
| 30 |
)
|
|
|
|
|
|
|
| 31 |
os.environ.setdefault("DATABASE_URL", f"sqlite:///{(ROOT / 'data' / 'atom.db').as_posix()}")
|
| 32 |
os.environ.setdefault("SKIP_USER_BOOTSTRAP", "true")
|
| 33 |
os.environ.setdefault("ATOM_MOCK_DATABASE", "false")
|
|
@@ -249,3 +251,4 @@ if __name__ == "__main__":
|
|
| 249 |
|
| 250 |
port = int(os.getenv("PORT", "7860"))
|
| 251 |
uvicorn.run("serve:app", host="0.0.0.0", port=port, factory=False)
|
|
|
|
|
|
| 28 |
"ALLOWED_ORIGINS",
|
| 29 |
"*,https://techprotrade-annator-atom.hf.space,http://localhost:7860",
|
| 30 |
)
|
| 31 |
+
if not os.environ.get('DATABASE_URL') and os.environ.get('NEON_DATABASE_URL'):
|
| 32 |
+
os.environ['DATABASE_URL'] = os.environ['NEON_DATABASE_URL']
|
| 33 |
os.environ.setdefault("DATABASE_URL", f"sqlite:///{(ROOT / 'data' / 'atom.db').as_posix()}")
|
| 34 |
os.environ.setdefault("SKIP_USER_BOOTSTRAP", "true")
|
| 35 |
os.environ.setdefault("ATOM_MOCK_DATABASE", "false")
|
|
|
|
| 251 |
|
| 252 |
port = int(os.getenv("PORT", "7860"))
|
| 253 |
uvicorn.run("serve:app", host="0.0.0.0", port=port, factory=False)
|
| 254 |
+
|