From abd523825230a6dafec32919eb95ca90ae88ff1e Mon Sep 17 00:00:00 2001 From: Frederik Spirgi Date: Wed, 9 Sep 2026 09:56:38 +0200 Subject: [PATCH] fix some issues --- public/account.html | 32 ++++++++++++++++++------------- public/app/chatDrawer.js | 2 +- public/app/themeController.js | 2 +- public/app/workspaceNavigation.js | 2 +- public/chat.html | 10 +++++----- public/discover.html | 12 ++++++------ public/home.html | 12 ++++++------ public/landing.html | 12 ++++++------ public/market.html | 10 +++++----- public/market.js | 8 ++++---- public/pages.css | 19 ++++++++++++++++++ public/pages.js | 31 +++++++++++++++++++++++++----- public/privacy.html | 10 +++++----- public/script.js | 12 ++++++------ public/settings.html | 10 +++++----- public/settings.js | 8 ++++---- public/tos.html | 10 +++++----- src/app.js | 4 ++++ test/app.test.js | 14 ++++++++++++++ 19 files changed, 142 insertions(+), 78 deletions(-) diff --git a/public/account.html b/public/account.html index c019bab..d0af319 100644 --- a/public/account.html +++ b/public/account.html @@ -5,11 +5,11 @@ Sign in · Elsewhere - - - - + + + @@ -29,11 +29,15 @@ Username - - +
+ +
+ + +
+
+ -

-

@@ -56,8 +62,8 @@ Privacy Terms - - + \ No newline at end of file diff --git a/public/app/chatDrawer.js b/public/app/chatDrawer.js index cb64a62..b6c845e 100644 --- a/public/app/chatDrawer.js +++ b/public/app/chatDrawer.js @@ -1,4 +1,4 @@ -import {chatDrawer, chatDrawerButton, chatDrawerCloseBtn} from "./dom.js"; +import {chatDrawer, chatDrawerButton, chatDrawerCloseBtn} from "./dom.js?v=20260909-account-controls"; let lastFocusedElement = null; diff --git a/public/app/themeController.js b/public/app/themeController.js index 661a76f..bb05c71 100644 --- a/public/app/themeController.js +++ b/public/app/themeController.js @@ -1,4 +1,4 @@ -import {themes} from "./constants.js"; +import {themes} from "./constants.js?v=20260909-account-controls"; export const THEME_STORAGE_KEY = "krishd-theme"; export const THEME_MODE_STORAGE_KEY = "krishd-theme-mode"; diff --git a/public/app/workspaceNavigation.js b/public/app/workspaceNavigation.js index 302d007..c19b859 100644 --- a/public/app/workspaceNavigation.js +++ b/public/app/workspaceNavigation.js @@ -1,4 +1,4 @@ -import {post} from './api.js'; +import {post} from './api.js?v=20260909-account-controls'; // Every page gets this control from the same server-rendered header. for (const button of document.querySelectorAll('[data-sign-out]')) { diff --git a/public/chat.html b/public/chat.html index 81aac0a..c6f33ef 100644 --- a/public/chat.html +++ b/public/chat.html @@ -7,10 +7,10 @@ - - + + - + @@ -352,7 +352,7 @@ - - + + diff --git a/public/discover.html b/public/discover.html index 83ab944..57e4df5 100644 --- a/public/discover.html +++ b/public/discover.html @@ -5,11 +5,11 @@ Discover personas · Elsewhere - - - - + + + @@ -30,8 +30,8 @@ Privacy Terms - - + \ No newline at end of file diff --git a/public/home.html b/public/home.html index ad18956..145b6b3 100644 --- a/public/home.html +++ b/public/home.html @@ -5,11 +5,11 @@ Home · Elsewhere - - - - + + + @@ -50,8 +50,8 @@

- - + \ No newline at end of file diff --git a/public/landing.html b/public/landing.html index d88a241..045ebb8 100644 --- a/public/landing.html +++ b/public/landing.html @@ -5,10 +5,10 @@ Elsewhere - - - - + + + + @@ -26,7 +26,7 @@ - - + + diff --git a/public/market.html b/public/market.html index c74d09c..cf62ffd 100644 --- a/public/market.html +++ b/public/market.html @@ -7,10 +7,10 @@ - - + + - + @@ -237,6 +237,6 @@ - - + + diff --git a/public/market.js b/public/market.js index 650ee4b..f2465a0 100644 --- a/public/market.js +++ b/public/market.js @@ -1,7 +1,7 @@ -import {get, post, put} from "./app/api.js"; -import {initializeAppearance} from "./app/themeController.js"; -import {clearStatus, setStatus} from "./app/statusNotice.js"; -import {populatePersonaForm, readPersonaForm} from "./app/personaForm.js"; +import {get, post, put} from "./app/api.js?v=20260909-account-controls"; +import {initializeAppearance} from "./app/themeController.js?v=20260909-account-controls"; +import {clearStatus, setStatus} from "./app/statusNotice.js?v=20260909-account-controls"; +import {populatePersonaForm, readPersonaForm} from "./app/personaForm.js?v=20260909-account-controls"; const $ = (id) => document.getElementById(id); const marketPersonaList = $("marketPersonaList"); diff --git a/public/pages.css b/public/pages.css index 794d1c6..7d3f6d7 100644 --- a/public/pages.css +++ b/public/pages.css @@ -49,3 +49,22 @@ .entry-door { width:65px; height:92px; border-width:1px; }.entry-door::before { inset:4px; }.entry-door span { inset:4px 4px 0 14px; border-width:1px; }.entry-door span::after { width:3px; height:3px; left:5px; }.entry-door::after { bottom:-8px; left:-10px; right:-10px; } .entry-copy h1 { font-size:2.8rem; }.entry-copy > p:not(.eyebrow) { margin:18px 0 24px; } } + +/* Account controls stay quiet; errors must be unmistakable. */ +.password-input { position:relative; } +#accountForm .password-input input { padding-right:50px; } +#accountForm .password-toggle { position:absolute; right:6px; top:50%; transform:translateY(-50%); width:36px; height:36px; min-height:36px; padding:0; border:0; border-radius:8px; background:transparent; color:var(--text-soft); box-shadow:none; } +#accountForm .password-toggle:hover { transform:translateY(-50%); background:var(--surface-hover); color:var(--text); } +.password-toggle svg { display:block; flex-shrink:0; } +.password-toggle .eye-slash { display:none; } +.password-toggle[aria-pressed="true"] .eye-slash { display:block; } +.account-status { display:flex; align-items:flex-start; gap:10px; margin:0; font-size:.9rem; line-height:1.6; color:var(--text-soft); } +.account-error-icon { display:none; flex:0 0 20px; margin-top:2px; } +.account-status[data-state="error"] { padding:13px 15px; border:1px solid #dfaaa5; border-left:4px solid #b3261e; border-radius:10px; background:#fff1f0; color:#92221b; font-weight:600; } +.account-status[data-state="error"] .account-error-icon { display:block; } +#accountForm input[aria-invalid="true"] { border-color:#b3261e; } +body[data-theme-mode="dark"] .account-status[data-state="error"] { background:#351f22; color:#ffb4ad; border-color:#925350; border-left-color:#ffb4ad; } +body[data-theme-mode="dark"] #accountForm input[aria-invalid="true"] { border-color:#ffb4ad; } +#accountForm .field > label { font-size:.82rem; font-weight:600; color:var(--text-soft); } +.account-status:focus { outline:none; } +#accountSwitch { margin-top:20px; } diff --git a/public/pages.js b/public/pages.js index 22ad43d..f9d543c 100644 --- a/public/pages.js +++ b/public/pages.js @@ -1,4 +1,4 @@ -import {get, post} from "/app/api.js"; +import {get, post} from "/app/api.js?v=20260909-account-controls"; const params = new URLSearchParams(location.search); function returnPath() { @@ -20,6 +20,14 @@ if (form) { const register = location.pathname === "/register"; const status = document.querySelector("#accountStatus"); const submit = document.querySelector("#accountSubmit"); + function showAccountStatus(message, error = false) { + status.dataset.state = error ? "error" : "pending"; + status.setAttribute("role", error ? "alert" : "status"); + status.setAttribute("aria-live", error ? "assertive" : "polite"); + status.classList.toggle("hidden", !message); + document.querySelector("#accountStatusText").textContent = message; + if (error) status.focus(); + } const label = register ? "Create account" : "Sign in"; document.title = `${label} · Elsewhere`; document.querySelector("#accountTitle").textContent = label; @@ -34,7 +42,7 @@ if (form) { document.querySelector("#accountHeadline").textContent = "Your next conversation starts here."; document.querySelector("#accountDescription").textContent = "Choose a username and a password of at least 6 characters."; } - if (session.error) status.textContent = session.error; + if (session.error) showAccountStatus(session.error, true); const switchText = document.querySelector("#accountSwitch"); switchText.append(register ? "Already have an account? " : "New to Elsewhere? "); const link = document.createElement("a"); @@ -44,18 +52,31 @@ if (form) { document.querySelector("#showPassword").addEventListener("click", event => { const show = form.elements.password.type === "password"; form.elements.password.type = show ? "text" : "password"; - event.currentTarget.textContent = show ? "Hide password" : "Show password"; + const label = show ? "Hide password" : "Show password"; + event.currentTarget.setAttribute("aria-label", label); + event.currentTarget.title = label; event.currentTarget.setAttribute("aria-pressed", String(show)); }); form.addEventListener("submit", async event => { event.preventDefault(); submit.disabled = true; - status.textContent = register ? "Creating your account…" : "Signing in…"; + form.elements.username.removeAttribute("aria-invalid"); + form.elements.username.removeAttribute("aria-describedby"); + showAccountStatus(register ? "Creating your account..." : "Signing in..."); const result = await post(register ? "/register" : "/login", { username: form.elements.username.value.trim(), password: form.elements.password.value, ...(register ? {inviteCode: form.elements.inviteCode.value.trim()} : {}) }); - if (result.error) { status.textContent = result.error; submit.disabled = false; return; } + if (result.error) { + const duplicate = register && result.error === "User already exists"; + if (duplicate) { + form.elements.username.setAttribute("aria-invalid", "true"); + form.elements.username.setAttribute("aria-describedby", "accountStatus"); + } + showAccountStatus(duplicate ? "That username is already taken. Choose another, or sign in below." : result.error, true); + submit.disabled = false; + return; + } location.assign(returnPath()); }); } diff --git a/public/privacy.html b/public/privacy.html index 7549fcc..1bcb7fb 100644 --- a/public/privacy.html +++ b/public/privacy.html @@ -5,10 +5,10 @@ Privacy Policy – DeepFake - - - - + + + + @@ -89,6 +89,6 @@

We may update this document. Changes will be posted with a “Last updated” date.

- + diff --git a/public/script.js b/public/script.js index ca2bc92..b1ef207 100644 --- a/public/script.js +++ b/public/script.js @@ -1,8 +1,8 @@ -import {del, get, post, put, stream} from "./app/api.js"; -import {closeChatDrawer, openChatDrawer} from "./app/chatDrawer.js"; -import {onboardingPrompts, requestedChatId} from "./app/constants.js"; -import {getModelProfile} from "./app/modelProfiles.js"; -import {applyTheme as applySharedTheme, applyThemeMode as applySharedThemeMode, readStoredAppearance} from "./app/themeController.js"; +import {del, get, post, put, stream} from "./app/api.js?v=20260909-account-controls"; +import {closeChatDrawer, openChatDrawer} from "./app/chatDrawer.js?v=20260909-account-controls"; +import {onboardingPrompts, requestedChatId} from "./app/constants.js?v=20260909-account-controls"; +import {getModelProfile} from "./app/modelProfiles.js?v=20260909-account-controls"; +import {applyTheme as applySharedTheme, applyThemeMode as applySharedThemeMode, readStoredAppearance} from "./app/themeController.js?v=20260909-account-controls"; import { activeChatTitle, activeUserPersonaStatus, @@ -99,7 +99,7 @@ import { brandLogoTargets, brandNameTargets, userPersonaList -} from "./app/dom.js"; +} from "./app/dom.js?v=20260909-account-controls"; const headerModelButton = document.getElementById("headerModelButton"); diff --git a/public/settings.html b/public/settings.html index 463c877..fe63981 100644 --- a/public/settings.html +++ b/public/settings.html @@ -7,10 +7,10 @@ - - + + - + @@ -298,6 +298,6 @@ - - + + diff --git a/public/settings.js b/public/settings.js index 83d4835..46917db 100644 --- a/public/settings.js +++ b/public/settings.js @@ -1,11 +1,11 @@ -import {del, get, post, put} from "./app/api.js"; +import {del, get, post, put} from "./app/api.js?v=20260909-account-controls"; import { applyTheme as applySharedTheme, applyThemeMode as applySharedThemeMode, initializeAppearance -} from "./app/themeController.js"; -import {clearStatus, setStatus} from "./app/statusNotice.js"; -import {populatePersonaForm, readPersonaForm} from "./app/personaForm.js"; +} from "./app/themeController.js?v=20260909-account-controls"; +import {clearStatus, setStatus} from "./app/statusNotice.js?v=20260909-account-controls"; +import {populatePersonaForm, readPersonaForm} from "./app/personaForm.js?v=20260909-account-controls"; const $ = (id) => document.getElementById(id); const settingsNotice = $("settingsNotice"); diff --git a/public/tos.html b/public/tos.html index c6decac..7de3f7b 100644 --- a/public/tos.html +++ b/public/tos.html @@ -5,10 +5,10 @@ Terms of Service – DeepFake - - - - + + + + @@ -47,6 +47,6 @@

We may modify these Terms at any time. Updates will be reflected with a new “Last updated” date.

- + diff --git a/src/app.js b/src/app.js index 6696c52..7d3c189 100644 --- a/src/app.js +++ b/src/app.js @@ -283,6 +283,10 @@ export function createApp(options = {}) { app.use(express.static(config.publicDir, { maxAge: config.staticMaxAge, etag: config.staticEtag, + setHeaders(res, filePath) { + // HTML and its modules must not drift apart after a deployment. + if (/\.(?:js|css)$/.test(filePath)) res.setHeader("Cache-Control", "no-cache"); + }, fallthrough: config.staticFallthrough })); app.use(createAuthRouter({repositories, authRateLimiters, config})); diff --git a/test/app.test.js b/test/app.test.js index 5d7b9eb..25ffcc6 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -1064,3 +1064,17 @@ test("page headers share navigation and keep session markup private", async () = assert.ok(html.includes('href="/login"')); } finally { await server.close(); } }); + +test("frontend scripts and styles revalidate across deployments", async () => { + const server = await startTestServer({config: {staticMaxAge: "1h"}}); + try { + for (const asset of ["/script.js?v=20260909-account-controls", "/app/dom.js?v=20260909-account-controls", "/pages.css?v=20260909-account-controls"]) { + const response = await fetch(server.baseUrl + asset); + assert.equal(response.status, 200); + assert.equal(response.headers.get("cache-control"), "no-cache"); + const etag = response.headers.get("etag"); + assert.ok(etag); + await response.text(); + } + } finally { await server.close(); } +});