Add a (simple) logo

This commit is contained in:
Olaf
2026-08-24 22:01:35 +02:00
parent 95992cc466
commit 4ae184f24c
20 changed files with 136 additions and 18 deletions
+5 -2
View File
@@ -8,6 +8,7 @@ LinkLog is a Firefox extension and Python web service for saving links with a ti
backend/ FastAPI application, services, database, and tests backend/ FastAPI application, services, database, and tests
frontend/ Jinja templates and browser-side assets frontend/ Jinja templates and browser-side assets
webextension/ Firefox Manifest V3 extension webextension/ Firefox Manifest V3 extension
Logo.svg Source logo artwork used by the web and extension interfaces
Dockerfile Backend container image Dockerfile Backend container image
docker-compose.yml App plus Traefik for local proxying docker-compose.yml App plus Traefik for local proxying
REQUIREMENTS.md Product requirements REQUIREMENTS.md Product requirements
@@ -77,13 +78,15 @@ Open these URLs:
The browser extension defaults to `http://localhost:8000`. The browser extension defaults to `http://localhost:8000`.
The supplied `Logo.svg` is bundled as `frontend/static/logo.svg` for web pages and `webextension/logo.svg` for the Firefox popup and settings page.
Appending a username to the root URL, such as `/alice`, opens that user's public feed and profile information. Appending a username to the root URL, such as `/alice`, opens that user's public feed and profile information.
Configuration APIs require a bearer token returned by the login endpoint. User configuration uses the identity in that token. Plugin administration additionally requires an administrator account; the development `alice` account is seeded as an administrator, while `bob` is a standard user. Configuration APIs require a bearer token returned by the login endpoint. User configuration uses the identity in that token. Plugin administration additionally requires an administrator account; the development `alice` account is seeded as an administrator, while `bob` is a standard user.
Users can change their password from the profile page. The current password is required, new passwords must contain at least 8 characters, and the endpoint is `PUT /api/user/password`. Users can change their password from the profile page. The current password is required, new passwords must contain at least 8 characters, and the endpoint is `PUT /api/user/password`.
On the profile page, the authenticated username is displayed as read-only. Users can upload a PNG, JPEG, GIF, or WebP avatar up to 2 MB; uploaded files are stored in the persistent data volume and served by the application. Bio and email fields remain empty until the user provides values. Mastodon settings default to the `mastodon.social` instance and the `From my #LinkLog: "` post prefix. On the profile page, the authenticated username is displayed as read-only. Users can upload a PNG, JPEG, GIF, or WebP avatar up to 2 MB; uploaded files are stored in the persistent data volume and served by the application. Bio and email fields remain empty until the user provides values. Mastodon settings default to the `mastodon.social` instance and the `From my #LinkLog: ` post prefix.
## Run Tests ## Run Tests
@@ -177,7 +180,7 @@ After logging in, open <http://localhost:8000/profile> and save the Mastodon set
- **Instance**: hostname or URL such as `mastodon.social` or `https://mastodon.social` - **Instance**: hostname or URL such as `mastodon.social` or `https://mastodon.social`
- **Access token**: a Mastodon API token with permission to create statuses - **Access token**: a Mastodon API token with permission to create statuses
- **Post prefix**: text placed immediately before the link; defaults to `From my #LinkLog: "` - **Post prefix**: text placed immediately before the link; defaults to `From my #LinkLog: `
Enable the plugin from the admin API or the admin page. New links are saved first and then posted to the configured instance at `/api/v1/statuses`. A Mastodon network failure does not undo the saved link. Enable the plugin from the admin API or the admin page. New links are saved first and then posted to the configured instance at `/api/v1/statuses`. A Mastodon network failure does not undo the saved link.
+18
View File
@@ -373,3 +373,21 @@ Create a page where users can add, delete, or edit labels - Labels can be edited
### Assistant outcome ### Assistant outcome
Added schema migration 4 with label ownership, authenticated user label CRUD endpoints and a `/labels` management page. Label creators can edit/delete their own labels, non-owners are denied, and administrators can delete any label including another user's labels. Added administrator label listing/deletion controls and regression coverage. The strict backend suite passed with 15 tests. Added schema migration 4 with label ownership, authenticated user label CRUD endpoints and a `/labels` management page. Label creators can edit/delete their own labels, non-owners are denied, and administrators can delete any label including another user's labels. Added administrator label listing/deletion controls and regression coverage. The strict backend suite passed with 15 tests.
### User
The profile page shows in red: "can't access property \"classList\", document.querySelector(...) is null"
### Assistant outcome
Found that `profile.js` still queried the removed standalone `#admin-link`, while the current hamburger menu uses `#auth-admin-link`. Updated the selector with an optional null guard, bumped the live asset to `profile.js?v=3`, rebuilt/verified the Docker service, and confirmed the profile route and full strict backend suite with 15 passing tests.
### User
The post prefix is now 'From my #LinkLog: "' make that 'From my #LinkLog: '
### Assistant outcome
Removed the trailing double quote from the default Mastodon post prefix in the publisher, profile UI, fallback, tests, and documentation. Removed the stale minified profile artifact and verified Mastodon/profile tests plus the full strict backend suite with 15 passing tests.
### User
Use the logo.svg in both the plugin as well as on the web page
### Assistant outcome
Bundled the supplied logo into the web static assets and Firefox extension, replaced text-only headers with accessible logo images, added responsive sizing, and verified all web pages, extension references, scripts, and the strict backend suite.
+3
View File
@@ -65,6 +65,9 @@
61. Make the tags filtering case incensitive (but maintain case of tags) 61. Make the tags filtering case incensitive (but maintain case of tags)
62. Display the date as "2026 June 18 - 20:22" 62. Display the date as "2026 June 18 - 20:22"
63. Create a page where users can add, delete, or edit labels - Labels can be edited and deleted by the same user that created them. The administrator can delete any label. 63. Create a page where users can add, delete, or edit labels - Labels can be edited and deleted by the same user that created them. The administrator can delete any label.
64. The profile page shows in red: "can't access property \"classList\", document.querySelector(...) is null"
65. The post prefix is now 'From my #LinkLog: "' make that 'From my #LinkLog: '
66. Use the logo.svg in both the plugin as well as on the web page
## Future entries ## Future entries
+1 -1
View File
@@ -4,7 +4,7 @@ from urllib.request import Request, urlopen
from backend.app.plugins.base import BasePlugin from backend.app.plugins.base import BasePlugin
DEFAULT_POST_PREFIX = 'From my #LinkLog: "' DEFAULT_POST_PREFIX = 'From my #LinkLog: '
class DefaultFrontendPlugin(BasePlugin): class DefaultFrontendPlugin(BasePlugin):
+6 -4
View File
@@ -234,6 +234,7 @@ 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 'src="/static/logo.svg"' in root_page.text
assert 'class="menu-toggle"' in root_page.text assert 'class="menu-toggle"' in root_page.text
assert 'id="auth-menu" class="auth-menu 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 'id="auth-home-link" href="/">Home</a>' in root_page.text
@@ -255,6 +256,7 @@ def test_public_and_admin_pages_render_html():
assert client.get('/login').status_code == 200 assert client.get('/login').status_code == 200
login_page = client.get('/login').text login_page = client.get('/login').text
assert 'Sign in' in login_page assert 'Sign in' in login_page
assert 'src="/static/logo.svg"' in login_page
assert 'id="auth-session" class="auth-session hidden"' in login_page assert 'id="auth-session" class="auth-session hidden"' in login_page
assert 'logout.js?v=3' in login_page assert 'logout.js?v=3' in login_page
assert client.get('/admin').status_code == 200 assert client.get('/admin').status_code == 200
@@ -300,7 +302,7 @@ def test_link_submission_posts_to_enabled_mastodon_plugin():
assert client.put('/api/user/plugins/mastodon', headers=headers, json={ assert client.put('/api/user/plugins/mastodon', headers=headers, json={
'instance': base_url, 'instance': base_url,
'access_token': 'test-token', 'access_token': 'test-token',
'post_prefix': 'From my #LinkLog: "', 'post_prefix': 'From my #LinkLog: ',
}).status_code == 200 }).status_code == 200
assert client.put('/api/admin/plugins/mastodon', headers=headers, json={'enabled': True}).status_code == 200 assert client.put('/api/admin/plugins/mastodon', headers=headers, json={'enabled': True}).status_code == 200
@@ -315,7 +317,7 @@ def test_link_submission_posts_to_enabled_mastodon_plugin():
assert received['path'] == '/api/v1/statuses' assert received['path'] == '/api/v1/statuses'
assert received['authorization'] == 'Bearer test-token' assert received['authorization'] == 'Bearer test-token'
assert received['body'] == { assert received['body'] == {
'status': 'A useful page\nWorth sharing\nFrom my #LinkLog: "https://example.com/useful #python #web', 'status': 'A useful page\nWorth sharing\nFrom my #LinkLog: https://example.com/useful #python #web',
} }
finally: finally:
server.shutdown() server.shutdown()
@@ -328,12 +330,12 @@ def test_plugin_config_can_be_saved_for_mastodon():
assert client.put('/api/user/plugins/mastodon', headers=headers, json={ assert client.put('/api/user/plugins/mastodon', headers=headers, json={
'instance': 'mastodon.social', 'instance': 'mastodon.social',
'access_token': 'demo-token', 'access_token': 'demo-token',
'post_prefix': 'From my #LinkLog: "', 'post_prefix': 'From my #LinkLog: ',
}).status_code == 200 }).status_code == 200
payload = client.get('/api/user/plugins/mastodon', headers=headers).json() payload = client.get('/api/user/plugins/mastodon', headers=headers).json()
assert payload['instance'] == 'mastodon.social' assert payload['instance'] == 'mastodon.social'
assert payload['post_prefix'] == 'From my #LinkLog: "' assert payload['post_prefix'] == 'From my #LinkLog: '
admin_update = client.put('/api/admin/plugins/mastodon', headers=headers, json={ admin_update = client.put('/api/admin/plugins/mastodon', headers=headers, json={
'enabled': True, 'enabled': True,
+1 -1
View File
@@ -34,7 +34,7 @@ def test_user_config_api_and_profile_page():
assert 'name="avatar" type="file"' in page_response.text assert 'name="avatar" type="file"' in page_response.text
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 'value="From my #LinkLog: "' in page_response.text
assert 'id="auth-menu" class="auth-menu hidden"' in page_response.text assert 'id="auth-menu" class="auth-menu hidden"' in page_response.text
assert 'id="auth-home-link" href="/">Home</a>' in page_response.text assert 'id="auth-home-link" href="/">Home</a>' in page_response.text
assert 'id="auth-profile-link" class="hidden"' in page_response.text assert 'id="auth-profile-link" class="hidden"' in page_response.text
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1599 972" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-771.901,-775.567)">
<g transform="matrix(5.14628,0,0,5.00692,41.3851,-6704.09)">
<g transform="matrix(239.668,0,0,239.668,455.228,1644.94)">
</g>
<text x="130.248px" y="1644.94px" style="font-family:'Asset-Regular', 'Asset';font-size:239.668px;fill:rgb(245,224,220);">L</text>
</g>
<g transform="matrix(0.139428,0,0,0.139428,656.217,1454.62)">
<g transform="matrix(1.02783,0,0,1,-21.8791,0)">
<g transform="matrix(1200,0,0,1200,11689,1802.1)">
</g>
<text x="786.091px" y="1802.1px" style="font-family:'Asset-Regular', 'Asset';font-size:1200px;fill:rgb(203,166,247);">L<tspan x="2432.05px 3507.29px 5279px 6980.56px 8652.17px 10164.4px " y="1802.1px 1802.1px 1802.1px 1802.1px 1802.1px 1802.1px ">inkLog</tspan></text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+2 -2
View File
@@ -5,7 +5,7 @@ const mastodonForm = document.querySelector('#mastodon-form');
const profileLogoutButton = document.querySelector('#logout-button'); const profileLogoutButton = document.querySelector('#logout-button');
const accessToken = localStorage.getItem('linklogAccessToken'); const accessToken = localStorage.getItem('linklogAccessToken');
const defaultMastodonInstance = 'mastodon.social'; const defaultMastodonInstance = 'mastodon.social';
const defaultPostPrefix = 'From my #LinkLog: "'; const defaultPostPrefix = 'From my #LinkLog: ';
function authHeaders(includeJson = false) { function authHeaders(includeJson = false) {
return { return {
@@ -33,7 +33,7 @@ async function loadProfile() {
avatarPreview.classList.remove('hidden'); avatarPreview.classList.remove('hidden');
} }
if (profile.is_admin) { if (profile.is_admin) {
document.querySelector('#admin-link').classList.remove('hidden'); document.querySelector('#auth-admin-link')?.classList.remove('hidden');
} }
profileLogoutButton.classList.remove('hidden'); profileLogoutButton.classList.remove('hidden');
} }
+14
View File
@@ -84,6 +84,15 @@ body::selection {
line-height: 1; line-height: 1;
} }
.site-logo {
display: block;
width: min(260px, 70vw);
height: 58px;
margin-bottom: 12px;
object-fit: contain;
object-position: left center;
}
.site-header p { .site-header p {
max-width: 34rem; max-width: 34rem;
margin: 14px 0 0; margin: 14px 0 0;
@@ -529,6 +538,11 @@ button:disabled {
font-size: 2.35rem; font-size: 2.35rem;
} }
.site-logo {
width: min(220px, 68vw);
height: 48px;
}
.header-row { .header-row {
align-items: center; align-items: center;
gap: 12px; gap: 12px;
+2 -1
View File
@@ -11,7 +11,8 @@
<div class="container"> <div class="container">
<div class="header-row"> <div class="header-row">
<div> <div>
<h1>LinkLog Admin</h1> <img class="site-logo" src="/static/logo.svg" alt="LinkLog" />
<h1>Admin</h1>
<p>Manage users and plugin configuration</p> <p>Manage users and plugin configuration</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
+1 -1
View File
@@ -11,7 +11,7 @@
<div class="container"> <div class="container">
<div class="header-row"> <div class="header-row">
<div> <div>
<h1>LinkLog</h1> <img class="site-logo" src="/static/logo.svg" alt="LinkLog" />
<p>Public link feed</p> <p>Public link feed</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
+1
View File
@@ -11,6 +11,7 @@
<div class="container"> <div class="container">
<div class="header-row"> <div class="header-row">
<div> <div>
<img class="site-logo" src="/static/logo.svg" alt="LinkLog" />
<h1>Labels</h1> <h1>Labels</h1>
<p>Manage your link labels</p> <p>Manage your link labels</p>
</div> </div>
+1 -1
View File
@@ -11,7 +11,7 @@
<div class="container"> <div class="container">
<div class="header-row"> <div class="header-row">
<div> <div>
<h1>Sign in</h1> <img class="site-logo" src="/static/logo.svg" alt="LinkLog" />
<p>Access your LinkLog settings</p> <p>Access your LinkLog settings</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
+6 -3
View File
@@ -10,7 +10,10 @@
<header class="site-header"> <header class="site-header">
<div class="container"> <div class="container">
<div class="header-row"> <div class="header-row">
<h1>Profile</h1> <div>
<img class="site-logo" src="/static/logo.svg" alt="LinkLog" />
<h1>Profile</h1>
</div>
<div class="header-actions"> <div class="header-actions">
<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button> <button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button>
<nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu"> <nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu">
@@ -84,7 +87,7 @@
</label> </label>
<label> <label>
Post prefix Post prefix
<input id="mastodon-post-prefix" name="post_prefix" type="text" value="From my #LinkLog: &quot;" /> <input id="mastodon-post-prefix" name="post_prefix" type="text" value="From my #LinkLog: " />
</label> </label>
<button type="submit">Save Mastodon settings</button> <button type="submit">Save Mastodon settings</button>
<p id="mastodon-status" class="status" role="status"></p> <p id="mastodon-status" class="status" role="status"></p>
@@ -93,6 +96,6 @@
</main> </main>
<script src="/static/auth-header.js?v=3"></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=3"></script>
</body> </body>
</html> </html>
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1599 972" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-771.901,-775.567)">
<g transform="matrix(5.14628,0,0,5.00692,41.3851,-6704.09)">
<g transform="matrix(239.668,0,0,239.668,455.228,1644.94)">
</g>
<text x="130.248px" y="1644.94px" style="font-family:'Asset-Regular', 'Asset';font-size:239.668px;fill:rgb(245,224,220);">L</text>
</g>
<g transform="matrix(0.139428,0,0,0.139428,656.217,1454.62)">
<g transform="matrix(1.02783,0,0,1,-21.8791,0)">
<g transform="matrix(1200,0,0,1200,11689,1802.1)">
</g>
<text x="786.091px" y="1802.1px" style="font-family:'Asset-Regular', 'Asset';font-size:1200px;fill:rgb(203,166,247);">L<tspan x="2432.05px 3507.29px 5279px 6980.56px 8652.17px 10164.4px " y="1802.1px 1802.1px 1802.1px 1802.1px 1802.1px 1802.1px ">inkLog</tspan></text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1599 972" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-771.901,-775.567)">
<g transform="matrix(5.14628,0,0,5.00692,41.3851,-6704.09)">
<g transform="matrix(239.668,0,0,239.668,455.228,1644.94)">
</g>
<text x="130.248px" y="1644.94px" style="font-family:'Asset-Regular', 'Asset';font-size:239.668px;fill:rgb(245,224,220);">L</text>
</g>
<g transform="matrix(0.139428,0,0,0.139428,656.217,1454.62)">
<g transform="matrix(1.02783,0,0,1,-21.8791,0)">
<g transform="matrix(1200,0,0,1200,11689,1802.1)">
</g>
<text x="786.091px" y="1802.1px" style="font-family:'Asset-Regular', 'Asset';font-size:1200px;fill:rgb(203,166,247);">L<tspan x="2432.05px 3507.29px 5279px 6980.56px 8652.17px 10164.4px " y="1802.1px 1802.1px 1802.1px 1802.1px 1802.1px 1802.1px ">inkLog</tspan></text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+9
View File
@@ -18,6 +18,15 @@ h1 {
margin-top: 0; margin-top: 0;
} }
.extension-logo {
display: block;
width: 190px;
height: 52px;
margin-bottom: 8px;
object-fit: contain;
object-position: left center;
}
label { label {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
+2 -1
View File
@@ -7,7 +7,8 @@
</head> </head>
<body> <body>
<main class="options-shell"> <main class="options-shell">
<h1>LinkLog Settings</h1> <img class="extension-logo" src="logo.svg" alt="LinkLog" />
<h1>Settings</h1>
<div id="status" class="status hidden" aria-live="polite"></div> <div id="status" class="status hidden" aria-live="polite"></div>
+9
View File
@@ -15,6 +15,15 @@ header h1 {
font-size: 1.2rem; font-size: 1.2rem;
} }
.extension-logo {
display: block;
width: 150px;
height: 42px;
margin-bottom: 8px;
object-fit: contain;
object-position: left center;
}
label { label {
display: block; display: block;
margin-bottom: 12px; margin-bottom: 12px;
+1 -1
View File
@@ -8,7 +8,7 @@
<body> <body>
<main class="popup-shell"> <main class="popup-shell">
<header> <header>
<h1>LinkLog</h1> <img class="extension-logo" src="logo.svg" alt="LinkLog" />
</header> </header>
<div id="status" class="status hidden" aria-live="polite"></div> <div id="status" class="status hidden" aria-live="polite"></div>