Add a (simple) logo
This commit is contained in:
@@ -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
|
||||
frontend/ Jinja templates and browser-side assets
|
||||
webextension/ Firefox Manifest V3 extension
|
||||
Logo.svg Source logo artwork used by the web and extension interfaces
|
||||
Dockerfile Backend container image
|
||||
docker-compose.yml App plus Traefik for local proxying
|
||||
REQUIREMENTS.md Product requirements
|
||||
@@ -77,13 +78,15 @@ Open these URLs:
|
||||
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
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
|
||||
|
||||
@@ -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`
|
||||
- **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.
|
||||
|
||||
|
||||
@@ -373,3 +373,21 @@ Create a page where users can add, delete, or edit labels - Labels can be edited
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
61. Make the tags filtering case incensitive (but maintain case of tags)
|
||||
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.
|
||||
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
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from urllib.request import Request, urlopen
|
||||
|
||||
from backend.app.plugins.base import BasePlugin
|
||||
|
||||
DEFAULT_POST_PREFIX = 'From my #LinkLog: "'
|
||||
DEFAULT_POST_PREFIX = 'From my #LinkLog: '
|
||||
|
||||
|
||||
class DefaultFrontendPlugin(BasePlugin):
|
||||
|
||||
@@ -234,6 +234,7 @@ def test_logout_revokes_token_and_admin_can_list_plugins():
|
||||
def test_public_and_admin_pages_render_html():
|
||||
root_page = client.get('/')
|
||||
assert 'LinkLog' in root_page.text
|
||||
assert 'src="/static/logo.svg"' 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-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
|
||||
login_page = client.get('/login').text
|
||||
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 'logout.js?v=3' in login_page
|
||||
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={
|
||||
'instance': base_url,
|
||||
'access_token': 'test-token',
|
||||
'post_prefix': 'From my #LinkLog: "',
|
||||
'post_prefix': 'From my #LinkLog: ',
|
||||
}).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['authorization'] == 'Bearer test-token'
|
||||
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:
|
||||
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={
|
||||
'instance': 'mastodon.social',
|
||||
'access_token': 'demo-token',
|
||||
'post_prefix': 'From my #LinkLog: "',
|
||||
'post_prefix': 'From my #LinkLog: ',
|
||||
}).status_code == 200
|
||||
|
||||
payload = client.get('/api/user/plugins/mastodon', headers=headers).json()
|
||||
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={
|
||||
'enabled': True,
|
||||
|
||||
@@ -34,7 +34,7 @@ def test_user_config_api_and_profile_page():
|
||||
assert 'name="avatar" type="file"' in page_response.text
|
||||
assert 'name="avatar_url"' not in page_response.text
|
||||
assert 'value="mastodon.social"' in page_response.text
|
||||
assert 'From my #LinkLog: "' 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-home-link" href="/">Home</a>' in page_response.text
|
||||
assert 'id="auth-profile-link" class="hidden"' in page_response.text
|
||||
|
||||
@@ -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 |
@@ -5,7 +5,7 @@ const mastodonForm = document.querySelector('#mastodon-form');
|
||||
const profileLogoutButton = document.querySelector('#logout-button');
|
||||
const accessToken = localStorage.getItem('linklogAccessToken');
|
||||
const defaultMastodonInstance = 'mastodon.social';
|
||||
const defaultPostPrefix = 'From my #LinkLog: "';
|
||||
const defaultPostPrefix = 'From my #LinkLog: ';
|
||||
|
||||
function authHeaders(includeJson = false) {
|
||||
return {
|
||||
@@ -33,7 +33,7 @@ async function loadProfile() {
|
||||
avatarPreview.classList.remove('hidden');
|
||||
}
|
||||
if (profile.is_admin) {
|
||||
document.querySelector('#admin-link').classList.remove('hidden');
|
||||
document.querySelector('#auth-admin-link')?.classList.remove('hidden');
|
||||
}
|
||||
profileLogoutButton.classList.remove('hidden');
|
||||
}
|
||||
|
||||
@@ -84,6 +84,15 @@ body::selection {
|
||||
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 {
|
||||
max-width: 34rem;
|
||||
margin: 14px 0 0;
|
||||
@@ -529,6 +538,11 @@ button:disabled {
|
||||
font-size: 2.35rem;
|
||||
}
|
||||
|
||||
.site-logo {
|
||||
width: min(220px, 68vw);
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
<div class="container">
|
||||
<div class="header-row">
|
||||
<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>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="container">
|
||||
<div class="header-row">
|
||||
<div>
|
||||
<h1>LinkLog</h1>
|
||||
<img class="site-logo" src="/static/logo.svg" alt="LinkLog" />
|
||||
<p>Public link feed</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<div class="container">
|
||||
<div class="header-row">
|
||||
<div>
|
||||
<img class="site-logo" src="/static/logo.svg" alt="LinkLog" />
|
||||
<h1>Labels</h1>
|
||||
<p>Manage your link labels</p>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="container">
|
||||
<div class="header-row">
|
||||
<div>
|
||||
<h1>Sign in</h1>
|
||||
<img class="site-logo" src="/static/logo.svg" alt="LinkLog" />
|
||||
<p>Access your LinkLog settings</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<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">
|
||||
<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">
|
||||
@@ -84,7 +87,7 @@
|
||||
</label>
|
||||
<label>
|
||||
Post prefix
|
||||
<input id="mastodon-post-prefix" name="post_prefix" type="text" value="From my #LinkLog: "" />
|
||||
<input id="mastodon-post-prefix" name="post_prefix" type="text" value="From my #LinkLog: " />
|
||||
</label>
|
||||
<button type="submit">Save Mastodon settings</button>
|
||||
<p id="mastodon-status" class="status" role="status"></p>
|
||||
@@ -93,6 +96,6 @@
|
||||
</main>
|
||||
<script src="/static/auth-header.js?v=3"></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>
|
||||
</html>
|
||||
|
||||
@@ -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 |
@@ -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,6 +18,15 @@ h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.extension-logo {
|
||||
display: block;
|
||||
width: 190px;
|
||||
height: 52px;
|
||||
margin-bottom: 8px;
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -15,6 +15,15 @@ header h1 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.extension-logo {
|
||||
display: block;
|
||||
width: 150px;
|
||||
height: 42px;
|
||||
margin-bottom: 8px;
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<body>
|
||||
<main class="popup-shell">
|
||||
<header>
|
||||
<h1>LinkLog</h1>
|
||||
<img class="extension-logo" src="logo.svg" alt="LinkLog" />
|
||||
</header>
|
||||
|
||||
<div id="status" class="status hidden" aria-live="polite"></div>
|
||||
|
||||
Reference in New Issue
Block a user