Header improvements

This commit is contained in:
Olaf
2026-08-24 20:17:46 +02:00
parent 12f3580f60
commit f96a4ab3ad
11 changed files with 183 additions and 93 deletions
+30
View File
@@ -295,3 +295,33 @@ Pressing signout (on the home page) does not log the user out
### Assistant outcome ### Assistant outcome
Found that the home page rendered the shared authenticated session header but did not load `logout.js`, so its sign-out button had no event handler. Added the logout handler to the home and login pages with cache-busted asset references. Verified real token revocation and the full strict backend suite with 12 passing tests. Found that the home page rendered the shared authenticated session header but did not load `logout.js`, so its sign-out button had no event handler. Added the logout handler to the home and login pages with cache-busted asset references. Verified real token revocation and the full strict backend suite with 12 passing tests.
### User
Instead of the buttons on the top of the page create a 'hamburger' dropdown menu. The menu items are sign in, profile, admin, and sign out with authenticated visibility rules.
### Assistant outcome
Replaced top-level authentication buttons on all web pages with a shared hamburger menu. The menu contains Sign in for signed-out users, Profile and Sign out for authenticated users, and Admin only for administrators. Added accessible toggle state, avatar/username identity display, responsive Mocha styling, and updated route tests. The strict backend suite passed with 12 tests and all browser scripts passed syntax checks.
### User
The menu items on the dropdown render behind <main class='container'>
### Assistant outcome
Fixed the dropdown stacking context by removing header overflow clipping, elevating the header above main content, and raising the menu z-index. Verified the served CSS rules, page rendering, browser scripts, and strict backend suite with 12 passing tests.
### User
The menu should not show the profile, admin and signout items when nobody is authenticated.
### Assistant outcome
Fixed CSS specificity so `.hidden` works inside the hamburger menu. The menu now shows only `Sign in` when no valid session exists; Profile, Admin, and Sign out remain hidden. Served CSS and authentication script checks passed, followed by the strict backend suite with 12 tests.
### User
Add one entry on top of the hamburger menu 'home' that always directs to the home page.
### Assistant outcome
Added `Home` as the first item in the shared hamburger menu on feed, user, login, profile, and admin pages. It always remains visible and links to `/`, independent of authentication state. All page smoke checks and the strict backend suite passed with 12 tests.
### User
where the hamburger menu shows the avatar and username, replace by username only and link to the /user/ page
### Assistant outcome
Replaced the hamburger menu avatar/username row with a username-only link. After authentication, the link dynamically targets `/<username>/`; the separate Profile, Admin, and Sign out menu items retain their session-based visibility. Removed the old menu avatar dependency, bumped the shared header asset to `v3`, and verified all pages plus the strict backend suite with 12 tests.
+5
View File
@@ -52,6 +52,11 @@
48. execute next todo 48. execute next todo
49. execute final todo (nummer 3) 49. execute final todo (nummer 3)
50. Pressing signout (on the home page) does not log the user out 50. Pressing signout (on the home page) does not log the user out
51. Instead of the buttons on the top of the page create a 'hamburger' dropdown menu. The menu items are sign in, profile, admin, and sign out with authenticated visibility rules.
52. The menu items on the dropdown render behind <main class='container'>
53. The menu should not show the profile, admin and signout items when nobody is authenticated.
54. Add one entry on top of the hamburger menu 'home' that always directs to the home page.
55. where the hamburger menu shows the avatar and username, replace by username only and link to the /user/ page
## Future entries ## Future entries
+12 -4
View File
@@ -172,8 +172,14 @@ def test_logout_revokes_token_and_admin_can_list_plugins():
def test_public_and_admin_pages_render_html(): def test_public_and_admin_pages_render_html():
root_page = client.get('/') root_page = client.get('/')
assert 'LinkLog' in root_page.text assert 'LinkLog' in root_page.text
assert 'class="login-button" href="/login"' in root_page.text assert 'class="menu-toggle"' in root_page.text
assert 'id="auth-session" class="auth-session hidden"' in root_page.text assert 'id="auth-menu" class="auth-menu hidden"' in root_page.text
assert 'id="auth-home-link" href="/">Home</a>' in root_page.text
assert '<a id="auth-username" class="user-name" href="/">' in root_page.text
assert 'id="auth-avatar"' not in root_page.text
assert 'id="auth-login-button" href="/login"' in root_page.text
assert 'id="auth-profile-link" class="hidden"' in root_page.text
assert 'id="auth-admin-link" class="hidden"' in root_page.text
assert 'logout.js?v=3' in root_page.text assert 'logout.js?v=3' in root_page.text
assert client.get('/alice').status_code == 200 assert client.get('/alice').status_code == 200
assert client.get('/alice/').status_code == 200 assert client.get('/alice/').status_code == 200
@@ -193,8 +199,10 @@ def test_public_and_admin_pages_render_html():
assert 'Admin' in admin_page assert 'Admin' in admin_page
assert 'id="admin-controls" class="hidden"' in admin_page assert 'id="admin-controls" class="hidden"' in admin_page
assert 'id="admin-auth-notice" class="auth-notice hidden"' in admin_page assert 'id="admin-auth-notice" class="auth-notice hidden"' in admin_page
assert 'id="admin-login-button" class="login-button" href="/login"' in admin_page assert 'id="auth-menu" class="auth-menu hidden"' in admin_page
assert 'id="auth-session" class="auth-session hidden"' in admin_page assert 'id="auth-home-link" href="/">Home</a>' in admin_page
assert '<a id="auth-username" class="user-name" href="/">' in admin_page
assert 'admin.js?v=3' in admin_page
feed_script = client.get('/static/feed.js?v=5').text feed_script = client.get('/static/feed.js?v=5').text
assert 'if (item.is_owner)' in feed_script assert 'if (item.is_owner)' in feed_script
+5 -3
View File
@@ -35,9 +35,11 @@ def test_user_config_api_and_profile_page():
assert 'name="avatar_url"' not in page_response.text assert 'name="avatar_url"' not in page_response.text
assert 'value="mastodon.social"' in page_response.text assert 'value="mastodon.social"' in page_response.text
assert 'From my #LinkLog: &quot;' in page_response.text assert 'From my #LinkLog: &quot;' in page_response.text
assert 'id="admin-link"' in page_response.text assert 'id="auth-menu" class="auth-menu hidden"' in page_response.text
assert 'class="login-button hidden"' in page_response.text assert 'id="auth-home-link" href="/">Home</a>' in page_response.text
assert 'id="auth-session" class="auth-session hidden"' in page_response.text assert 'id="auth-profile-link" class="hidden"' in page_response.text
assert '<a id="auth-username" class="user-name" href="/">' in page_response.text
assert 'id="auth-avatar"' not in page_response.text
bob_login = client.post('/api/auth/login', json={ bob_login = client.post('/api/auth/login', json={
'username': 'bob', 'username': 'bob',
-8
View File
@@ -4,8 +4,6 @@ const userList = document.querySelector('#user-list');
const userForm = document.querySelector('#user-form'); const userForm = document.querySelector('#user-form');
const adminControls = document.querySelector('#admin-controls'); const adminControls = document.querySelector('#admin-controls');
const adminAuthNotice = document.querySelector('#admin-auth-notice'); const adminAuthNotice = document.querySelector('#admin-auth-notice');
const adminLoginButton = document.querySelector('#admin-login-button');
const adminLogoutButton = document.querySelector('#logout-button');
const accessToken = localStorage.getItem('linklogAccessToken'); const accessToken = localStorage.getItem('linklogAccessToken');
function authHeaders(includeJson = false) { function authHeaders(includeJson = false) {
@@ -66,22 +64,16 @@ async function loadUsers() {
function showAdminState(isAdmin) { function showAdminState(isAdmin) {
adminControls.classList.toggle('hidden', !isAdmin); adminControls.classList.toggle('hidden', !isAdmin);
adminAuthNotice.classList.toggle('hidden', isAdmin); adminAuthNotice.classList.toggle('hidden', isAdmin);
adminLoginButton.classList.toggle('hidden', isAdmin);
adminLogoutButton.classList.toggle('hidden', !accessToken);
} }
function showSignedOutState() { function showSignedOutState() {
showAdminState(false); showAdminState(false);
adminAuthNotice.innerHTML = 'Administrator sign-in required. <a href="/login">Sign in</a>'; adminAuthNotice.innerHTML = 'Administrator sign-in required. <a href="/login">Sign in</a>';
adminLoginButton.classList.remove('hidden');
adminLogoutButton.classList.add('hidden');
} }
function showUnauthorizedState() { function showUnauthorizedState() {
showAdminState(false); showAdminState(false);
adminAuthNotice.textContent = 'You are signed in, but you are not authorized to access this page.'; adminAuthNotice.textContent = 'You are signed in, but you are not authorized to access this page.';
adminLoginButton.classList.add('hidden');
adminLogoutButton.classList.remove('hidden');
} }
async function loadAdminState() { async function loadAdminState() {
+20 -15
View File
@@ -1,34 +1,39 @@
(() => { (() => {
const loginButton = document.querySelector('#auth-login-button'); const loginButton = document.querySelector('#auth-login-button');
const profileLink = document.querySelector('#auth-profile-link');
const adminLink = document.querySelector('#auth-admin-link');
const session = document.querySelector('#auth-session'); const session = document.querySelector('#auth-session');
const avatar = document.querySelector('#auth-avatar');
const username = document.querySelector('#auth-username'); const username = document.querySelector('#auth-username');
const token = localStorage.getItem('linklogAccessToken'); const token = localStorage.getItem('linklogAccessToken');
if (!loginButton || !session || !avatar || !username) return; const menu = document.querySelector('#auth-menu');
const menuToggle = document.querySelector('.menu-toggle');
const logoutButton = document.querySelector('#logout-button');
if (!loginButton || !profileLink || !adminLink || !session || !username || !menu || !menuToggle || !logoutButton) return;
menuToggle.addEventListener('click', () => {
const isOpen = !menu.classList.contains('hidden');
menu.classList.toggle('hidden', isOpen);
menuToggle.setAttribute('aria-expanded', String(!isOpen));
});
function showSignedOut() { function showSignedOut() {
loginButton.classList.remove('hidden'); loginButton.classList.remove('hidden');
profileLink.classList.add('hidden');
adminLink.classList.add('hidden');
session.classList.add('hidden'); session.classList.add('hidden');
logoutButton.classList.add('hidden');
} }
function showSignedIn(user) { function showSignedIn(user) {
loginButton.classList.add('hidden'); loginButton.classList.add('hidden');
profileLink.classList.remove('hidden');
adminLink.classList.toggle('hidden', !user.is_admin);
session.classList.remove('hidden'); session.classList.remove('hidden');
logoutButton.classList.remove('hidden');
username.textContent = user.username || ''; username.textContent = user.username || '';
const initial = (user.username || 'U').slice(0, 1).toUpperCase(); username.href = `/${encodeURIComponent(user.username || '')}/`;
avatar.textContent = initial;
if (user.avatar_url) {
const image = document.createElement('img');
image.src = user.avatar_url;
image.alt = `${user.username || 'User'} avatar`;
image.addEventListener('error', () => {
image.remove();
avatar.textContent = initial;
});
avatar.textContent = '';
avatar.appendChild(image);
}
} }
if (!token) { if (!token) {
+62 -33
View File
@@ -51,7 +51,7 @@ body::selection {
.site-header { .site-header {
position: relative; position: relative;
overflow: hidden; z-index: 20;
padding: 56px 0 48px; padding: 56px 0 48px;
background: background:
linear-gradient(115deg, rgba(203, 166, 247, 0.18), transparent 45%), linear-gradient(115deg, rgba(203, 166, 247, 0.18), transparent 45%),
@@ -103,18 +103,67 @@ body::selection {
flex: 0 0 auto; flex: 0 0 auto;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
position: relative;
}
.menu-toggle {
min-width: 0;
padding: 10px 13px;
border-color: var(--surface-2);
background: var(--surface-0);
color: var(--text);
}
.menu-toggle::before {
content: '\2630';
margin-right: 7px;
font-size: 1.1em;
}
.auth-menu {
position: absolute;
z-index: 30;
top: calc(100% + 10px);
right: 0;
display: grid;
min-width: 190px;
gap: 4px;
padding: 8px;
background: var(--surface-0);
border: 1px solid var(--surface-1);
border-radius: 10px;
box-shadow: var(--shadow);
}
.auth-menu a,
.auth-menu button {
display: block;
width: 100%;
min-width: 0;
padding: 9px 10px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--text);
text-align: left;
text-decoration: none;
}
.auth-menu a:hover,
.auth-menu button:hover {
background: var(--surface-1);
color: var(--lavender);
}
.auth-menu .hidden {
display: none;
} }
.auth-session { .auth-session {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 9px; padding: 7px 10px;
} border-bottom: 1px solid var(--border);
.auth-session .avatar {
width: 32px;
height: 32px;
flex-basis: 32px;
} }
.auth-session .user-name { .auth-session .user-name {
@@ -124,28 +173,8 @@ body::selection {
white-space: nowrap; white-space: nowrap;
} }
.login-button {
flex: 0 0 auto;
margin-top: 2px;
padding: 10px 16px;
border: 1px solid var(--surface-2);
border-radius: 8px;
background: var(--surface-0);
color: var(--text);
font-weight: 700;
text-decoration: none;
transition: background 160ms ease, border-color 160ms ease;
}
.login-button:hover {
border-color: var(--lavender);
background: var(--surface-1);
color: var(--text);
}
.logout-button { .logout-button {
min-width: 0; min-width: 0;
padding: 10px 13px;
border-color: rgba(243, 139, 168, 0.45); border-color: rgba(243, 139, 168, 0.45);
background: transparent; background: transparent;
color: var(--red); color: var(--red);
@@ -158,6 +187,8 @@ body::selection {
} }
main.container { main.container {
position: relative;
z-index: 1;
padding-top: 28px; padding-top: 28px;
padding-bottom: 64px; padding-bottom: 64px;
} }
@@ -462,21 +493,19 @@ button:disabled {
} }
.header-actions { .header-actions {
align-items: stretch; align-items: flex-end;
flex-direction: column;
} }
.auth-session { .auth-session {
flex-direction: column; align-items: flex-start;
} }
.login-button { .menu-toggle {
padding: 8px 11px; padding: 8px 11px;
font-size: 0.9rem; font-size: 0.9rem;
} }
.logout-button { .logout-button {
padding: 8px 11px;
font-size: 0.9rem; font-size: 0.9rem;
} }
+13 -8
View File
@@ -15,12 +15,17 @@
<p>Manage users and plugin configuration</p> <p>Manage users and plugin configuration</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<a id="admin-login-button" class="login-button" href="/login">Sign in</a> <button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button>
<div id="auth-session" class="auth-session hidden"> <nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu">
<div id="auth-avatar" class="avatar"></div> <a id="auth-home-link" href="/">Home</a>
<span id="auth-username" class="user-name"></span> <a id="auth-login-button" href="/login">Sign in</a>
<button id="logout-button" class="logout-button" type="button">Sign out</button> <a id="auth-profile-link" class="hidden" href="/profile">Profile</a>
</div> <a id="auth-admin-link" class="hidden" href="/admin">Admin</a>
<div id="auth-session" class="auth-session hidden">
<a id="auth-username" class="user-name" href="/"></a>
</div>
<button id="logout-button" class="logout-button hidden" type="button">Sign out</button>
</nav>
</div> </div>
</div> </div>
</div> </div>
@@ -60,8 +65,8 @@
</section> </section>
</div> </div>
</main> </main>
<script src="/static/auth-header.js?v=1"></script> <script src="/static/auth-header.js?v=3"></script>
<script src="/static/logout.js?v=2"></script> <script src="/static/logout.js?v=2"></script>
<script src="/static/admin.js?v=2"></script> <script src="/static/admin.js?v=3"></script>
</body> </body>
</html> </html>
+12 -7
View File
@@ -15,12 +15,17 @@
<p>Public link feed</p> <p>Public link feed</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<a id="auth-login-button" class="login-button" href="/login">Sign in</a> <button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button>
<div id="auth-session" class="auth-session hidden"> <nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu">
<div id="auth-avatar" class="avatar"></div> <a id="auth-home-link" href="/">Home</a>
<span id="auth-username" class="user-name"></span> <a id="auth-login-button" href="/login">Sign in</a>
<button id="logout-button" class="logout-button" type="button">Sign out</button> <a id="auth-profile-link" class="hidden" href="/profile">Profile</a>
</div> <a id="auth-admin-link" class="hidden" href="/admin">Admin</a>
<div id="auth-session" class="auth-session hidden">
<a id="auth-username" class="user-name" href="/"></a>
</div>
<button id="logout-button" class="logout-button hidden" type="button">Sign out</button>
</nav>
</div> </div>
</div> </div>
</div> </div>
@@ -61,7 +66,7 @@
<section id="feed" class="feed" aria-live="polite"></section> <section id="feed" class="feed" aria-live="polite"></section>
</main> </main>
<script src="/static/auth-header.js?v=1"></script> <script src="/static/auth-header.js?v=3"></script>
<script src="/static/logout.js?v=3"></script> <script src="/static/logout.js?v=3"></script>
<script src="/static/feed.js?v=5"></script> <script src="/static/feed.js?v=5"></script>
</body> </body>
+12 -7
View File
@@ -15,12 +15,17 @@
<p>Access your LinkLog settings</p> <p>Access your LinkLog settings</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<a id="auth-login-button" class="login-button" href="/login">Sign in</a> <button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button>
<div id="auth-session" class="auth-session hidden"> <nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu">
<div id="auth-avatar" class="avatar"></div> <a id="auth-home-link" href="/">Home</a>
<span id="auth-username" class="user-name"></span> <a id="auth-login-button" href="/login">Sign in</a>
<button id="logout-button" class="logout-button" type="button">Sign out</button> <a id="auth-profile-link" class="hidden" href="/profile">Profile</a>
</div> <a id="auth-admin-link" class="hidden" href="/admin">Admin</a>
<div id="auth-session" class="auth-session hidden">
<a id="auth-username" class="user-name" href="/"></a>
</div>
<button id="logout-button" class="logout-button hidden" type="button">Sign out</button>
</nav>
</div> </div>
</div> </div>
</div> </div>
@@ -41,7 +46,7 @@
</form> </form>
</section> </section>
</main> </main>
<script src="/static/auth-header.js?v=1"></script> <script src="/static/auth-header.js?v=3"></script>
<script src="/static/logout.js?v=3"></script> <script src="/static/logout.js?v=3"></script>
<script src="/static/login.js"></script> <script src="/static/login.js"></script>
</body> </body>
+12 -8
View File
@@ -12,19 +12,23 @@
<div class="header-row"> <div class="header-row">
<h1>Profile</h1> <h1>Profile</h1>
<div class="header-actions"> <div class="header-actions">
<a id="admin-link" class="login-button hidden" href="/admin">Admin</a> <button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button>
<div id="auth-session" class="auth-session hidden"> <nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu">
<div id="auth-avatar" class="avatar"></div> <a id="auth-home-link" href="/">Home</a>
<span id="auth-username" class="user-name"></span> <a id="auth-login-button" href="/login">Sign in</a>
<button id="logout-button" class="logout-button" type="button">Sign out</button> <a id="auth-profile-link" class="hidden" href="/profile">Profile</a>
</div> <a id="auth-admin-link" class="hidden" href="/admin">Admin</a>
<div id="auth-session" class="auth-session hidden">
<a id="auth-username" class="user-name" href="/"></a>
</div>
<button id="logout-button" class="logout-button hidden" type="button">Sign out</button>
</nav>
</div> </div>
</div> </div>
</div> </div>
</header> </header>
<main class="container"> <main class="container">
<p class="auth-notice">Sign in at <a href="/login">/login</a> to manage your settings.</p>
<section class="link-item settings-panel"> <section class="link-item settings-panel">
<h2>Profile Settings</h2> <h2>Profile Settings</h2>
<form id="profile-form"> <form id="profile-form">
@@ -86,7 +90,7 @@
</form> </form>
</section> </section>
</main> </main>
<script src="/static/auth-header.js?v=1"></script> <script src="/static/auth-header.js?v=3"></script>
<script src="/static/logout.js?v=2"></script> <script src="/static/logout.js?v=2"></script>
<script src="/static/profile.js?v=2"></script> <script src="/static/profile.js?v=2"></script>
</body> </body>