Squash Odysseus development history

This commit is contained in:
pewdiepie-archdaemon
2026-09-11 06:04:19 +00:00
parent c9dd68d890
commit 84aa9a91de
871 changed files with 265870 additions and 27854 deletions
+75 -33
View File
@@ -199,6 +199,22 @@
button:hover { background: color-mix(in srgb, var(--red) 66%, black); }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.error { color: #e55; font-size: 0.85rem; margin-bottom: 0.75rem; display: none; }
/* On phones the error used to sit above the form, which puts it behind the
keyboard after a failed sign-in. Reorder only the mobile form so the
message stays directly below the password field. */
@media (max-width: 600px) {
.card { display: flex; flex-direction: column; }
#authForm { display: contents; }
#authForm > label[for="username"] { order: 1; }
#authForm > .login-username-wrap { order: 2; }
#authForm > label[for="password"] { order: 3; }
#authForm > .login-password-wrap { order: 4; }
#error { order: 5; margin-top: 0.15rem; margin-bottom: 0.75rem; scroll-margin-bottom: 1rem; }
#authForm > #confirmGroup { order: 6; }
#authForm > #submitBtn { order: 7; }
#toggleArea { order: 8; }
}
.toggle { text-align: center; margin-top: calc(1rem + 4px); font-size: 0.85rem; color: color-mix(in srgb, var(--fg) 50%, transparent); }
.toggle a { color: var(--red); cursor: pointer; text-decoration: none; }
.toggle a:hover { text-decoration: underline; }
@@ -254,19 +270,6 @@
font-family: 'Fira Code', monospace;
pointer-events: none; user-select: none;
}
/* Whirlpool-style spinner shown in the submit button while login /
account-creation work is in flight — replaces the previous "Loading…"
text. Two-color ring (muted base + accent arc) rotates continuously. */
.login-spinner {
display: inline-block;
width: 16px; height: 16px;
border: 2px solid color-mix(in srgb, var(--fg, #fff) 22%, transparent);
border-top-color: var(--accent, var(--red, #e8a830));
border-radius: 50%;
animation: login-spin 0.7s linear infinite;
vertical-align: -3px;
}
@keyframes login-spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
@@ -280,7 +283,7 @@
<form id="authForm" autocomplete="on">
<label for="username">Username</label>
<div class="pw-wrapper">
<div class="pw-wrapper login-username-wrap">
<input id="username" name="username" type="text" required autofocus autocomplete="username">
<label class="remember-toggle" id="rememberToggle" title="Remember me">
<input type="checkbox" class="remember-check" id="remember" checked aria-label="Remember me">
@@ -289,7 +292,7 @@
</div>
<label for="password">Password</label>
<div class="pw-wrapper">
<div class="pw-wrapper login-password-wrap">
<input id="password" name="password" type="password" required autocomplete="current-password">
<button type="button" class="pw-toggle" id="pwToggle" tabindex="-1" aria-label="Show password">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><line x1="8" y1="16" x2="16" y2="8"/><line x1="8" y1="8" x2="16" y2="16"/></svg>
@@ -325,7 +328,7 @@
const vr = await fetch(appUrl('/api/version'));
if (vr.ok) {
const vd = await vr.json();
document.getElementById('version-label').textContent = 'v' + vd.version;
document.getElementById('version-label').textContent = 'v' + (vd.build || vd.version);
}
} catch(e) {}
@@ -345,11 +348,54 @@
const toggleArea = document.getElementById('toggleArea');
const toggleLink = document.getElementById('toggleLink');
const toggleText = document.getElementById('toggleText');
const spinnerModulePromise = import(appUrl('/static/js/spinner.js'));
let submitSpinner = null;
let submitSpinnerToken = 0;
let mode = 'login'; // 'login' | 'signup' | 'setup'
let signupAllowed = false;
let policy = { password_min_length: 8, reserved_usernames: [] };
function submitLabel() {
if (form._totpMode) return 'Verify';
if (mode === 'setup') return 'Create Admin Account';
if (mode === 'signup') return 'Create Account';
return 'Sign In';
}
function restoreSubmit() {
submitSpinnerToken += 1;
submitSpinner?.destroy?.();
submitSpinner = null;
submitBtn.textContent = submitLabel();
submitBtn.disabled = false;
}
function showAuthError(message) {
errEl.textContent = message;
errEl.style.display = 'block';
// With the virtual keyboard open, keep the mobile-only message visible
// below the password field instead of leaving it behind the keyboard.
if (window.matchMedia('(max-width: 600px)').matches) {
requestAnimationFrame(() => errEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }));
}
}
function showSubmitWhirlpool() {
const token = ++submitSpinnerToken;
submitBtn.replaceChildren();
submitBtn.disabled = true;
spinnerModulePromise.then((spinnerModule) => {
if (token !== submitSpinnerToken) return;
submitSpinner = spinnerModule.createWhirlpool(16);
submitSpinner.element.style.cssText = 'display:inline-block;vertical-align:middle;width:16px;height:16px;margin:0;';
submitSpinner.element.setAttribute('aria-hidden', 'true');
submitBtn.appendChild(submitSpinner.element);
}).catch(() => {
if (token === submitSpinnerToken) submitBtn.textContent = '…';
});
}
const rememberToggle = document.getElementById('rememberToggle');
function setMode(m) {
@@ -424,6 +470,7 @@
const code = totpInput ? totpInput.value.trim() : '';
if (!code) { totpInput.focus(); submitBtn.disabled = false; return; }
const remember = document.getElementById('remember').checked;
showSubmitWhirlpool();
try {
const res = await fetch(appUrl('/api/auth/login'), {
method: 'POST',
@@ -437,9 +484,8 @@
form._totpMode = false;
finishLogin();
} catch (err) {
errEl.textContent = err.message;
errEl.style.display = 'block';
submitBtn.disabled = false;
showAuthError(err.message);
restoreSubmit();
}
return;
}
@@ -448,25 +494,24 @@
if (mode === 'setup' || mode === 'signup') {
const confirm = document.getElementById('confirmPassword').value;
if (password !== confirm) {
errEl.textContent = 'Passwords do not match';
errEl.style.display = 'block';
showAuthError('Passwords do not match');
submitBtn.disabled = false;
return;
}
if (password.length < policy.password_min_length) {
errEl.textContent = `Password must be at least ${policy.password_min_length} characters`;
errEl.style.display = 'block';
showAuthError(`Password must be at least ${policy.password_min_length} characters`);
submitBtn.disabled = false;
return;
}
if (policy.reserved_usernames.includes(username.toLowerCase())) {
errEl.textContent = 'This username is reserved';
errEl.style.display = 'block';
showAuthError('This username is reserved');
submitBtn.disabled = false;
return;
}
}
showSubmitWhirlpool();
// Setup or signup first
if (mode === 'setup' || mode === 'signup') {
const endpoint = mode === 'setup' ? appUrl('/api/auth/setup') : appUrl('/api/auth/signup');
@@ -480,9 +525,8 @@
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Account creation failed');
} catch (err) {
errEl.textContent = err.message;
errEl.style.display = 'block';
submitBtn.disabled = false;
showAuthError(err.message);
restoreSubmit();
return;
}
}
@@ -494,7 +538,6 @@
if (_rem) localStorage.setItem('odysseus-last-user', username);
else localStorage.removeItem('odysseus-last-user');
sessionStorage.removeItem('ody-session-active');
submitBtn.innerHTML = '<span class="login-spinner" aria-hidden="true"></span>';
submitBtn.disabled = true;
Promise.all([
fetch(appUrl('/api/sessions'), { credentials: 'same-origin' }).then(r => r.json()),
@@ -523,7 +566,7 @@
let data = await doLogin();
// 2FA required — show TOTP input
if (data.requires_totp) {
submitBtn.disabled = false;
restoreSubmit();
// Only add TOTP input once
if (document.getElementById('totp-input')) return;
form._totpMode = true;
@@ -539,9 +582,8 @@
}
finishLogin();
} catch (err) {
errEl.textContent = err.message;
errEl.style.display = 'block';
submitBtn.disabled = false;
showAuthError(err.message);
restoreSubmit();
return;
}
});