5 Commits
Author SHA1 Message Date
olaf 7bd64b870c Tried to fix a broke filter.
Build LinkLog Development Image / development-image (push) Successful in 10s
2026-08-27 22:54:22 +02:00
olaf 9494d2b119 Red theme and regression 2026-08-27 22:22:41 +02:00
olaf 3661d0b4b7 Logo links to home page 2026-08-27 22:14:32 +02:00
olaf d433a305f5 Fixed mastodon checkbox on new entry page 2026-08-27 22:08:30 +02:00
olaf 1c2d1b71ff tag placement in new-entry page 2026-08-27 21:45:38 +02:00
20 changed files with 208 additions and 61 deletions
+13
View File
@@ -9,8 +9,21 @@
* Ability to minimize settings panels to only show headers in the Admin and Profile interfaces for easy navigation * Ability to minimize settings panels to only show headers in the Admin and Profile interfaces for easy navigation
* Styled settings panel headers with distinct theme accent colors for visual distinction * Styled settings panel headers with distinct theme accent colors for visual distinction
* Settings panels are collapsed by default when opening the Admin and Profile pages * Settings panels are collapsed by default when opening the Admin and Profile pages
* Show checked-by-default Mastodon publishing on New Entry only for configured users
* Made every web header logo link to the home page
* Added a high-contrast Red color theme
* Brightened the Red theme surfaces and deepened its crimson accents
* Allow every user to filter the feed using every available tag or label
### Fixed ### Fixed
* Fixed element ID conflict on the Admin page so Available Themes load correctly * Fixed element ID conflict on the Admin page so Available Themes load correctly
* Positioned new-entry tag checkboxes after their label text
* Kept new-entry tag checkboxes immediately left of their labels at all viewport sizes
* Prevented the new-entry form grid from placing tag checkboxes above their labels
* Kept unconfigured New Entry Mastodon publishing controls hidden
* Restored the home-page header layout, keeping the action controls above the filter toolbar
* Made the tag filter refresh the feed directly when its selection changes
* Cache-busted the feed script to ensure browsers load the responsive tag filter
* Fixed feed initialization so tag filtering does not receive promise results as a selected tag [Still no complete fix]
## Version v0.1.1 ## Version v0.1.1
### Features ### Features
+78
View File
@@ -1340,3 +1340,81 @@ On the admin page under Available Themes, the page does not load available theme
### Assistant outcome ### Assistant outcome
Fixed element ID conflict on `admin.html` where both the header theme submenu and the admin form container used `id="theme-options"`. Renamed the admin form container to `id="admin-theme-options"` and updated `admin.js` to target `#admin-theme-options`. Available themes now load correctly on the admin page. Passed all 55 tests. Fixed element ID conflict on `admin.html` where both the header theme submenu and the admin form container used `id="theme-options"`. Renamed the admin form container to `id="admin-theme-options"` and updated `admin.js` to target `#admin-theme-options`. Available themes now load correctly on the admin page. Passed all 55 tests.
### User
on the new-entry page the checkbox should be following the labels - now they are positioned above.
### Assistant outcome
Reordered the new-entry tag control markup so each label is followed by its checkbox. JavaScript syntax validation and the focused API test module passed with 35 tests.
### User
The checkboxes should be immediately to the left of the label not below left
### Assistant outcome
Restored checkbox-before-label ordering and added a scoped compact-sizing rule for new-entry tag checkboxes. This prevents the mobile global input-width rule from placing a checkbox on its own line. JavaScript syntax validation and the focused API test module passed with 35 tests.
### User
The checkbox takes the full width of the grid which forces the label to be printed underneath instead of directly next to it.
### Assistant outcome
Fixed the actual layout conflict: `.form-section label` uses a grid layout whose selector overrode the tag label's flex layout. Added the more specific `.form-section label.tag-checkbox` inline-flex rule and reset its checkbox margin, keeping each checkbox directly left of its tag text. The focused API test module passed with 35 tests and `git diff --check` passed.
### User
Only show the Mastodon publishing on new page if the user has Mastodon posting configured. Then default the checkbox to yes
### Assistant outcome
Added a safe `configured` status to the authenticated Mastodon configuration response. New Entry now hides its Mastodon publishing section unless the current user has both an instance and access token configured; when shown, its checkbox defaults to checked. Link creation accepts `post_to_mastodon`, allowing an unchecked control to skip Mastodon while preserving dispatch of other plugins. Added posting and configuration regression coverage.
### User
In the currently running contaier the user olaf has no mastodon configured but the mastodon checkbox is still visible
### Assistant outcome
Found that `.form-section { display: grid; }` appeared later in the stylesheet than `.hidden { display: none; }`, overriding the initial hidden state of the Mastodon fieldset. Added `.form-section.hidden { display: none; }`, so the publishing control stays hidden until the authenticated configuration check explicitly reveals it.
### User
When clicking on the logo always go to the home page
### Assistant outcome
Wrapped the header logo in each web template with an accessible home-page link. Extended the heading font selector for the new logo-link wrapper, preserving existing header styling. Confirmed all eight templates contain exactly one home-linked logo and the focused API test module passed with 36 tests.
### User
Add a red collor theme
### Assistant outcome
Added a Red theme to the theme catalog with a high-contrast crimson palette in the frontend stylesheet. It is enabled by default on new installations and is available to select under Admin > Available themes. Extended the theme-selection regression test to verify the public theme API returns it when enabled.
### User
Make the theme brighter and darker red
### Assistant outcome
Refined the Red theme palette with brighter white and red-tinted surfaces, plus deeper saturated crimson accents, borders, and shadows while preserving dark high-contrast text. The focused theme API test and whitespace validation passed.
### User
Since checkin d433a305f5cd8ccdafde8a0f7764462e8c167162 on the home page the filter toolbar moved so that the menu button and new entry are not in the top left anylonger - revert to the previous layout
### Assistant outcome
Confirmed the referenced commit did not change the feed header, then identified the current template had the toolbar outside its `.header-tools` wrapper. Restored the prior header structure: the New Entry and menu controls remain grouped above the filter toolbar on the right side of the home-page header. Added a rendered-page regression assertion for `.header-tools`.
### User
I want every user to filter on every available tag/label
### Assistant outcome
Separated feed filtering from label management: `/api/tags` now returns every tag/label to all callers, allowing complete filtering of the public feed. `/api/user/labels` remains ownership-scoped, so users still cannot manage other users' labels. Updated tag visibility regression coverage for authenticated and anonymous callers.
### User
tag filter doesn't seem to respond when pressed
### Assistant outcome
Updated the tag-filter change handler to pass the newly selected tag directly to the feed refresh instead of relying on the browser to persist and immediately reread the preference cookie. This makes filtering work even when cookie persistence is unavailable or delayed. Cache-busted the feed script to `feed.js?v=10` and added a regression assertion for the direct refresh path.
### User
The tag filter still does not allow me to select tags, even though the raw html contains the correct content. Also the user filter and the sort functionality work too
### Assistant outcome
Verified the live running container with Chromium and Playwright: selecting `#AI` changes the tag dropdown value, writes the preference cookie, and renders only matching feed entries. The deployed script already contains the direct-refresh handler, so cache-busted it again to `feed.js?v=11` and rebuilt the Compose service. Confirmed the live page serves the new asset version.
### User
I believe tag selection was still working for v0.1.0 - it is still broken
### Assistant outcome
Found the actual regression in the direct-refresh update: `Promise.all([loadUsers(), loadTags()]).then(loadFeed)` passed its results array into the new `selectedTag` parameter. The feed then attempted to call `toLowerCase()` on that array during initialization. Restored the v0.1.0 callback shape with `.then(() => loadFeed())`, retained direct selected-tag refreshes, added a regression assertion, and cache-busted the feed asset to `feed.js?v=12`.
+13
View File
@@ -239,6 +239,19 @@
218. In the Admin interface allow to minimize each settings pannel to only show its header, In order to easely navigate the page. 218. In the Admin interface allow to minimize each settings pannel to only show its header, In order to easely navigate the page.
219. Clicking on the collapse button doesn't toggle 219. Clicking on the collapse button doesn't toggle
218. In the Admin interface allow to minimize each settings pannel to only show its header, In order to easely navigate the page. 218. In the Admin interface allow to minimize each settings pannel to only show its header, In order to easely navigate the page.
223. On the new-entry page the checkbox should be following the labels - now they are positioned above.
224. The checkboxes should be immediately to the left of the label not below left
225. The checkbox takes the full width of the grid which forces the label to be printed underneath instead of directly next to it.
226. Only show the Mastodon publishing on new page if the user has Mastodon posting configured. Then default the checkbox to yes
227. In the currently running contaier the user olaf has no mastodon configured but the mastodon checkbox is still visible
228. When clicking on the logo always go to the home page
229. Add a red collor theme
230. Make the theme brighter and darker red
231. Since checkin d433a305f5cd8ccdafde8a0f7764462e8c167162 on the home page the filter toolbar moved so that the menu button and new entry are not in the top left anylonger - revert to the previous layout
232. I want every user to filter on every available tag/label
233. tag filter doesn't seem to respond when pressed
234. The tag filter still does not allow me to select tags, even though the raw html contains the correct content. Also the user filter and the sort functionality work too
235. I believe tag selection was still working for v0.1.0 - it is still broken
## Future entries ## Future entries
+8 -5
View File
@@ -6,7 +6,6 @@ from fastapi import APIRouter, Depends, Header, HTTPException, Response, status
import logging import logging
from pydantic import BaseModel from pydantic import BaseModel
from backend.app.api.dependencies import get_optional_current_user
from backend.app.services.link_service import create_link, delete_link, find_owned_link_by_title_url, get_link_tags, get_owned_link, list_public_links, list_tags, mark_mastodon_posted, update_link from backend.app.services.link_service import create_link, delete_link, find_owned_link_by_title_url, get_link_tags, get_owned_link, list_public_links, list_tags, mark_mastodon_posted, update_link
from backend.app.database import get_connection from backend.app.database import get_connection
from backend.app.services.plugin_manager import plugin_manager from backend.app.services.plugin_manager import plugin_manager
@@ -24,6 +23,7 @@ class LinkCreate(BaseModel):
comment: str = '' comment: str = ''
timestamp: str | None = None timestamp: str | None = None
tags: list[str] = [] tags: list[str] = []
post_to_mastodon: bool = True
class LinkUpdate(BaseModel): class LinkUpdate(BaseModel):
@@ -34,9 +34,8 @@ class LinkUpdate(BaseModel):
@router.get('/tags') @router.get('/tags')
def available_tags(user: dict | None = Depends(get_optional_current_user)): def available_tags():
user_id = user['id'] if user else None return list_tags()
return list_tags(user_id=user_id)
@router.get('/scrape') @router.get('/scrape')
@@ -89,7 +88,11 @@ def create_link_endpoint(payload: LinkCreate, response: Response, authorization:
raise HTTPException(status_code=422, detail=str(error)) from error raise HTTPException(status_code=422, detail=str(error)) from error
if duplicate: if duplicate:
response.status_code = status.HTTP_200_OK response.status_code = status.HTTP_200_OK
plugin_results = plugin_manager.dispatch({'type': 'link_created', **record}) plugin_results = plugin_manager.dispatch({
'type': 'link_created',
'post_to_mastodon': payload.post_to_mastodon,
**record,
})
mastodon_result = next((result for result in plugin_results if result.get('plugin') == 'mastodon'), None) mastodon_result = next((result for result in plugin_results if result.get('plugin') == 'mastodon'), None)
if mastodon_result and mastodon_result.get('status') == 'posted': if mastodon_result and mastodon_result.get('status') == 'posted':
mark_mastodon_posted(record['id'], info['user_id'], mastodon_result.get('post_id')) mark_mastodon_posted(record['id'], info['user_id'], mastodon_result.get('post_id'))
+2
View File
@@ -390,6 +390,8 @@ def get_user_plugin_config(plugin_name: str, user: dict = Depends(get_current_us
return {} return {}
config = json.loads(row['config']) if row['config'] else {} config = json.loads(row['config']) if row['config'] else {}
if plugin_name == 'mastodon':
config['configured'] = bool(config.get('instance') and config.get('access_token'))
if config.get('access_token'): if config.get('access_token'):
config.pop('access_token') config.pop('access_token')
return config return config
+2 -25
View File
@@ -227,32 +227,9 @@ def list_public_users():
return [row['username'] for row in rows] return [row['username'] for row in rows]
def list_tags(user_id: str | None = None): def list_tags():
with get_connection() as conn: with get_connection() as conn:
if user_id: rows = conn.execute('SELECT name FROM tags ORDER BY name').fetchall()
rows = conn.execute(
'''
SELECT tags.name
FROM tags
LEFT JOIN users ON users.id = tags.created_by
WHERE tags.created_by IS NULL
OR tags.created_by = ?
OR users.is_admin = 1
ORDER BY tags.name
''',
(user_id,),
).fetchall()
else:
rows = conn.execute(
'''
SELECT tags.name
FROM tags
LEFT JOIN users ON users.id = tags.created_by
WHERE tags.created_by IS NULL
OR users.is_admin = 1
ORDER BY tags.name
''',
).fetchall()
tags = [] tags = []
seen = set() seen = set()
for row in rows: for row in rows:
+3
View File
@@ -34,6 +34,9 @@ class MastodonPlugin(BasePlugin):
return True return True
def handle_event(self, event): def handle_event(self, event):
if not event.get('post_to_mastodon', True):
return {'status': 'skipped', 'plugin': self.name, 'reason': 'not_requested'}
config = dict(self.config) config = dict(self.config)
user_id = event.get('user_id') user_id = event.get('user_id')
if user_id: if user_id:
+1
View File
@@ -16,6 +16,7 @@ THEMES = {
'dracula': {'label': 'Dracula', 'description': 'A vivid dark theme with high-contrast accents.'}, 'dracula': {'label': 'Dracula', 'description': 'A vivid dark theme with high-contrast accents.'},
'nord': {'label': 'Nord', 'description': 'A cool, muted blue-gray theme.'}, 'nord': {'label': 'Nord', 'description': 'A cool, muted blue-gray theme.'},
'solarized': {'label': 'Solarized', 'description': 'A balanced theme available in a light style.'}, 'solarized': {'label': 'Solarized', 'description': 'A balanced theme available in a light style.'},
'red': {'label': 'Red', 'description': 'A warm crimson theme with high-contrast surfaces.'},
} }
DEFAULT_ENABLED_THEMES = tuple(THEMES) DEFAULT_ENABLED_THEMES = tuple(THEMES)
+26 -8
View File
@@ -150,10 +150,10 @@ def test_configuration_requires_authentication_and_admin_role():
def test_admin_can_select_multiple_themes(): def test_admin_can_select_multiple_themes():
headers = login_headers() headers = login_headers()
response = client.put('/api/admin/themes', headers=headers, json={ response = client.put('/api/admin/themes', headers=headers, json={
'themes': ['plain-day', 'plain-night', 'latte', 'frappe', 'macchiato', 'mocha', 'dracula', 'nord', 'solarized'], 'themes': ['plain-day', 'plain-night', 'latte', 'frappe', 'macchiato', 'mocha', 'dracula', 'nord', 'solarized', 'red'],
}) })
assert response.status_code == 200 assert response.status_code == 200
assert response.json()['enabled'] == ['plain-day', 'plain-night', 'latte', 'frappe', 'macchiato', 'mocha', 'dracula', 'nord', 'solarized'] assert response.json()['enabled'] == ['plain-day', 'plain-night', 'latte', 'frappe', 'macchiato', 'mocha', 'dracula', 'nord', 'solarized', 'red']
public_response = client.get('/api/public/themes') public_response = client.get('/api/public/themes')
assert public_response.status_code == 200 assert public_response.status_code == 200
assert [theme['id'] for theme in public_response.json()] == response.json()['enabled'] assert [theme['id'] for theme in public_response.json()] == response.json()['enabled']
@@ -564,17 +564,17 @@ def test_label_visibility_isolation_and_grandfathering():
assert '#Cybersecurity' in charlie_label_names assert '#Cybersecurity' in charlie_label_names
assert '#BobOnlyLabel' not in charlie_label_names assert '#BobOnlyLabel' not in charlie_label_names
# Bob views /api/tags (authenticated): sees Bob tag & default/grandfathered, NOT Charlie tag # Every user can filter by every available tag, including another user's label.
bob_tags = client.get('/api/tags', headers=bob_headers).json() bob_tags = client.get('/api/tags', headers=bob_headers).json()
assert '#BobOnlyLabel' in bob_tags assert '#BobOnlyLabel' in bob_tags
assert '#GrandfatheredLabel' in bob_tags assert '#GrandfatheredLabel' in bob_tags
assert '#CharlieOnlyLabel' not in bob_tags assert '#CharlieOnlyLabel' in bob_tags
# Anonymous views /api/tags: sees default/grandfathered, NOT Bob or Charlie tag # The public feed filter has the same complete tag catalog.
anon_tags = client.get('/api/tags').json() anon_tags = client.get('/api/tags').json()
assert '#GrandfatheredLabel' in anon_tags assert '#GrandfatheredLabel' in anon_tags
assert '#BobOnlyLabel' not in anon_tags assert '#BobOnlyLabel' in anon_tags
assert '#CharlieOnlyLabel' not in anon_tags assert '#CharlieOnlyLabel' in anon_tags
# Bob cannot edit or delete grandfathered label # Bob cannot edit or delete grandfathered label
assert client.put(f"/api/user/labels/{grandfathered_id}", headers=bob_headers, json={'name': '#RenamedGrandfathered'}).status_code == 404 assert client.put(f"/api/user/labels/{grandfathered_id}", headers=bob_headers, json={'name': '#RenamedGrandfathered'}).status_code == 404
@@ -792,6 +792,7 @@ def test_public_and_admin_pages_render_html():
assert 'alice' in user_page assert 'alice' in user_page
assert 'data-user-filter="alice"' in user_page assert 'data-user-filter="alice"' in user_page
assert 'profile-summary' in user_page assert 'profile-summary' in user_page
assert '<div class="header-tools">' in user_page
feed_script = TestClient(app).get('/static/feed.js?v=4').text feed_script = TestClient(app).get('/static/feed.js?v=4').text
assert 'window.location.assign(selectedUser ? `/${encodeURIComponent(selectedUser)}/` : \'/\')' in feed_script assert 'window.location.assign(selectedUser ? `/${encodeURIComponent(selectedUser)}/` : \'/\')' in feed_script
assert client.get('/login').status_code == 200 assert client.get('/login').status_code == 200
@@ -827,7 +828,11 @@ def test_public_and_admin_pages_render_html():
assert 'if (item.is_owner && !showIdentity)' in feed_script assert 'if (item.is_owner && !showIdentity)' in feed_script
assert 'deleteEntry(item, deleteButton)' in feed_script assert 'deleteEntry(item, deleteButton)' in feed_script
assert 'postToMastodon(item, mastodonButton)' in feed_script assert 'postToMastodon(item, mastodonButton)' in feed_script
assert 'tag.toLowerCase() === pref.tag.toLowerCase()' in feed_script assert 'tag.toLowerCase() === activeTag.toLowerCase()' in feed_script
assert 'loadFeed(event.target.value)' in feed_script
assert 'Promise.all([loadUsers(), loadTags()]).then(() => loadFeed())' in feed_script
assert "fetch('/api/tags', {" in feed_script
assert 'Authorization: `Bearer ${accessToken}`' in feed_script
assert 'return `${date.getFullYear()} ${months[date.getMonth()]} ${date.getDate()} - ${hours}:${minutes}`' in feed_script assert 'return `${date.getFullYear()} ${months[date.getMonth()]} ${date.getDate()} - ${hours}:${minutes}`' in feed_script
assert "entryMeta.className = 'entry-meta'" in feed_script assert "entryMeta.className = 'entry-meta'" in feed_script
assert "meta.className = 'meta'" in feed_script assert "meta.className = 'meta'" in feed_script
@@ -889,6 +894,18 @@ def test_link_submission_posts_to_enabled_mastodon_plugin():
server.server_close() server.server_close()
def test_link_submission_can_skip_mastodon_posting():
with patch('backend.app.api.links.plugin_manager.dispatch', return_value=[]) as dispatch:
response = client.post('/api/links', headers=login_headers(), json={
'title': 'Private share',
'url': 'https://example.com/private-share',
'post_to_mastodon': False,
})
assert response.status_code == 201
assert dispatch.call_args.args[0]['post_to_mastodon'] is False
def test_mastodon_post_without_title_omits_source_line(): def test_mastodon_post_without_title_omits_source_line():
from backend.app.services.plugin_manager import MastodonPlugin from backend.app.services.plugin_manager import MastodonPlugin
@@ -922,6 +939,7 @@ def test_plugin_config_can_be_saved_for_mastodon():
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: '
assert payload['configured'] is True
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,
+9 -6
View File
@@ -73,7 +73,9 @@ async function loadUsers() {
} }
async function loadTags() { async function loadTags() {
const response = await fetch('/api/tags'); const response = await fetch('/api/tags', {
headers: accessToken ? {Authorization: `Bearer ${accessToken}`} : {},
});
if (!response.ok) throw new Error('Could not load tags'); if (!response.ok) throw new Error('Could not load tags');
const tags = await response.json(); const tags = await response.json();
availableTags = tags; availableTags = tags;
@@ -254,7 +256,7 @@ function showEditForm(article, item) {
article.appendChild(form); article.appendChild(form);
} }
async function loadFeed() { async function loadFeed(selectedTag = null) {
const routeUser = document.body.dataset.userFilter; const routeUser = document.body.dataset.userFilter;
const endpoint = routeUser const endpoint = routeUser
? `/api/public/feed/${encodeURIComponent(routeUser)}` ? `/api/public/feed/${encodeURIComponent(routeUser)}`
@@ -266,13 +268,14 @@ async function loadFeed() {
let items = data || []; let items = data || [];
const pref = readPreferences(); const pref = readPreferences();
const activeTag = selectedTag ?? pref.tag;
if (pref.user && !routeUser) { if (pref.user && !routeUser) {
items = items.filter((item) => (item.user?.username || '').toLowerCase() === pref.user.toLowerCase()); items = items.filter((item) => (item.user?.username || '').toLowerCase() === pref.user.toLowerCase());
} }
if (pref.tag) { if (activeTag) {
items = items.filter((item) => item.tags?.some((tag) => tag.toLowerCase() === pref.tag.toLowerCase())); items = items.filter((item) => item.tags?.some((tag) => tag.toLowerCase() === activeTag.toLowerCase()));
} }
if (pref.sort === 'oldest') { if (pref.sort === 'oldest') {
@@ -304,9 +307,9 @@ function syncPreferences() {
tagFilter.addEventListener('change', (event) => { tagFilter.addEventListener('change', (event) => {
const next = { ...readPreferences(), tag: event.target.value }; const next = { ...readPreferences(), tag: event.target.value };
writePreferences(next); writePreferences(next);
loadFeed(); loadFeed(event.target.value);
}); });
} }
syncPreferences(); syncPreferences();
Promise.all([loadUsers(), loadTags()]).then(loadFeed).catch(() => loadFeed()); Promise.all([loadUsers(), loadTags()]).then(() => loadFeed()).catch(() => loadFeed());
+17 -1
View File
@@ -9,6 +9,7 @@
const commentInput = document.getElementById('comment-input'); const commentInput = document.getElementById('comment-input');
const newTagsInput = document.getElementById('new-tags-input'); const newTagsInput = document.getElementById('new-tags-input');
const existingTagsEl = document.getElementById('existing-tags'); const existingTagsEl = document.getElementById('existing-tags');
const mastodonPublishing = document.getElementById('mastodon-publishing');
const mastodonEnabledCheckbox = document.getElementById('mastodon-enabled'); const mastodonEnabledCheckbox = document.getElementById('mastodon-enabled');
const scrapeButton = document.getElementById('scrape-button'); const scrapeButton = document.getElementById('scrape-button');
const scrapeStatus = document.getElementById('scrape-status'); const scrapeStatus = document.getElementById('scrape-status');
@@ -46,7 +47,7 @@
.then((user) => { .then((user) => {
currentUser = user; currentUser = user;
entryForm.classList.remove('hidden'); entryForm.classList.remove('hidden');
loadTags(); Promise.all([loadTags(), loadMastodonPublishing()]);
}) })
.catch(() => { .catch(() => {
localStorage.removeItem('linklogAccessToken'); localStorage.removeItem('linklogAccessToken');
@@ -66,6 +67,20 @@
} }
} }
async function loadMastodonPublishing() {
try {
const response = await fetch('/api/user/plugins/mastodon', {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) return;
const config = await response.json();
mastodonEnabledCheckbox.checked = Boolean(config.configured);
mastodonPublishing.classList.toggle('hidden', !config.configured);
} catch (error) {
console.error('Could not load Mastodon configuration:', error);
}
}
// Render tag checkboxes // Render tag checkboxes
function renderTags() { function renderTags() {
existingTagsEl.innerHTML = ''; existingTagsEl.innerHTML = '';
@@ -171,6 +186,7 @@
url, url,
comment, comment,
tags, tags,
post_to_mastodon: mastodonEnabledCheckbox.checked,
}), }),
}); });
+22 -1
View File
@@ -95,6 +95,15 @@
--border: rgba(7, 54, 66, 0.18); --shadow: 0 18px 50px rgba(7, 54, 66, 0.14); --border: rgba(7, 54, 66, 0.18); --shadow: 0 18px 50px rgba(7, 54, 66, 0.14);
} }
:root[data-theme='red'] {
--base: #fffafa; --mantle: #ffedef; --crust: #fbd6da;
--surface-0: #ffffff; --surface-1: #ffe2e6; --surface-2: #f4bbc4;
--text: #350810; --subtext: #5c1724; --muted: #8c4653;
--mauve: #9e1737; --lavender: #86132d; --blue: #216f91;
--teal: #087567; --peach: #b84324; --red: #a70d2d;
--border: rgba(83, 10, 25, 0.2); --shadow: 0 18px 50px rgba(122, 8, 33, 0.2);
}
*, *,
*::before, *::before,
*::after { *::after {
@@ -169,7 +178,7 @@ body::selection {
object-position: left center; object-position: left center;
} }
.site-header .site-logo + h1, .site-header .site-logo-link + h1,
.feed-link { .feed-link {
font-family: 'Asset', 'Space Grotesk', sans-serif; font-family: 'Asset', 'Space Grotesk', sans-serif;
} }
@@ -1006,6 +1015,10 @@ button:disabled {
gap: 8px; gap: 8px;
} }
.form-section.hidden {
display: none;
}
.form-section label { .form-section label {
display: grid; display: grid;
gap: 6px; gap: 6px;
@@ -1065,7 +1078,15 @@ button:disabled {
user-select: none; user-select: none;
} }
.form-section label.tag-checkbox {
display: inline-flex;
}
.tag-checkbox input { .tag-checkbox input {
width: auto;
min-width: 0;
flex: 0 0 auto;
margin: 0;
cursor: pointer; cursor: pointer;
} }
+1 -1
View File
@@ -13,7 +13,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" /> <a class="site-logo-link" href="/" aria-label="LinkLog home"><img class="site-logo" src="/static/logo.svg" alt="LinkLog" /></a>
<h1>About</h1> <h1>About</h1>
<p>A quiet place for the links worth keeping.</p> <p>A quiet place for the links worth keeping.</p>
</div> </div>
+1 -1
View File
@@ -13,7 +13,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" /> <a class="site-logo-link" href="/" aria-label="LinkLog home"><img class="site-logo" src="/static/logo.svg" alt="LinkLog" /></a>
<h1>Admin</h1> <h1>Admin</h1>
<p>Manage users and plugin configuration</p> <p>Manage users and plugin configuration</p>
</div> </div>
+2 -2
View File
@@ -13,7 +13,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" /> <a class="site-logo-link" href="/" aria-label="LinkLog home"><img class="site-logo" src="/static/logo.svg" alt="LinkLog" /></a>
<p>Public link feed</p> <p>Public link feed</p>
</div> </div>
<div class="header-tools"> <div class="header-tools">
@@ -86,6 +86,6 @@
<script src="/static/auth-header.js?v=3"></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/theme.js?v=1"></script> <script src="/static/theme.js?v=1"></script>
<script src="/static/feed.js?v=9"></script> <script src="/static/feed.js?v=12"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -13,7 +13,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" /> <a class="site-logo-link" href="/" aria-label="LinkLog home"><img class="site-logo" src="/static/logo.svg" alt="LinkLog" /></a>
<h1>Labels</h1> <h1>Labels</h1>
<p>Manage your link labels</p> <p>Manage your link labels</p>
</div> </div>
+1 -1
View File
@@ -13,7 +13,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" /> <a class="site-logo-link" href="/" aria-label="LinkLog home"><img class="site-logo" src="/static/logo.svg" alt="LinkLog" /></a>
<p>Access your LinkLog settings</p> <p>Access your LinkLog settings</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
+5 -6
View File
@@ -13,10 +13,9 @@
<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" /> <a class="site-logo-link" href="/" aria-label="LinkLog home"><img class="site-logo" src="/static/logo.svg" alt="LinkLog" /></a>
<p>New Entry</p> <p>New Entry</p>
</div> </div>
<div class="header-tools">
<div class="header-actions"> <div class="header-actions">
<button id="new-entry-button" class="new-entry-button hidden" type="button"><a href="/new-entry">New entry</a></button> <button id="new-entry-button" class="new-entry-button hidden" type="button"><a href="/new-entry">New entry</a></button>
<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>
@@ -80,11 +79,11 @@
</label> </label>
</fieldset> </fieldset>
<fieldset class="form-section"> <fieldset id="mastodon-publishing" class="form-section hidden">
<legend>Mastodon Publishing</legend> <legend>Mastodon Publishing</legend>
<label> <label>
<input id="mastodon-enabled" type="checkbox" /> <input id="mastodon-enabled" type="checkbox" checked />
Post to Mastodon (if configured) Post to Mastodon
</label> </label>
</fieldset> </fieldset>
@@ -102,6 +101,6 @@
</footer> </footer>
<script src="/static/auth-header.js"></script> <script src="/static/auth-header.js"></script>
<script src="/static/new-entry.js"></script> <script src="/static/new-entry.js?v=2"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -13,7 +13,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" /> <a class="site-logo-link" href="/" aria-label="LinkLog home"><img class="site-logo" src="/static/logo.svg" alt="LinkLog" /></a>
<h1>Configure LinkLog</h1> <h1>Configure LinkLog</h1>
<p>Create the first administrator and test email delivery.</p> <p>Create the first administrator and test email delivery.</p>
</div> </div>
+1 -1
View File
@@ -15,7 +15,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" /> <a class="site-logo-link" href="/" aria-label="LinkLog home"><img class="site-logo" src="/static/logo.svg" alt="LinkLog" /></a>
<h1>Profile</h1> <h1>Profile</h1>
</div> </div>
<div class="header-actions"> <div class="header-actions">