From defeaef94aa9452ee42de38c5fd5909dd19ea904 Mon Sep 17 00:00:00 2001 From: Andy Morrell Date: Fri, 30 Oct 2026 19:54:28 +0000 Subject: [PATCH 1/6] feat(web): add retry button for pending and failed themes Show a Retry button on library items that need attention or are in ThemerrDB but not downloaded yet. The button posts to a new /api/tasks/retry endpoint, which reuses the existing retry_items checks (cached item, enabled server and updates, ignored libraries) and queues the item for processing. --- locale/themerr.po | 16 ++++-- src/common/server_ui.py | 26 +++++++++ src/themerr/mcp_tools.py | 2 +- tests/functional/test_server_ui.py | 91 ++++++++++++++++++++++++++++++ tests/web/dashboard.test.mjs | 15 ++++- web/css/custom.css | 4 ++ web/js/dashboard.js | 10 ++++ web/templates/home.html | 1 + 8 files changed, 159 insertions(+), 6 deletions(-) diff --git a/locale/themerr.po b/locale/themerr.po index 3e318445..b75fd9ce 100644 --- a/locale/themerr.po +++ b/locale/themerr.po @@ -1453,19 +1453,27 @@ msgstr "" msgid "Theme not installed yet" msgstr "" -#: web/templates/home.html:82 +#: web/templates/home.html:81 +msgid "Retry theme for" +msgstr "" + +#: web/templates/home.html:81 +msgid "Retry" +msgstr "" + +#: web/templates/home.html:83 msgid "Edit" msgstr "" -#: web/templates/home.html:82 +#: web/templates/home.html:83 msgid "Add" msgstr "" -#: web/templates/home.html:89 +#: web/templates/home.html:90 msgid "No titles match." msgstr "" -#: web/templates/home.html:89 +#: web/templates/home.html:90 msgid "Try another search or reset your filters." msgstr "" diff --git a/src/common/server_ui.py b/src/common/server_ui.py index 6ac820ff..686e65a0 100644 --- a/src/common/server_ui.py +++ b/src/common/server_ui.py @@ -257,6 +257,32 @@ def refresh(payload=Depends(_payload)): ) +@router.api_route('/api/tasks/retry', methods=['POST'], name='server_ui.retry', response_model=None) +def retry(payload=Depends(_payload)): + """Queue one cached library item for another theme attempt. + + Returns + ------- + Response + Dispatch confirmation, or a fixed client error when the item cannot be retried. + """ + from themerr import mcp_tools + server_id = payload.get('server_id') + item_id = payload.get('item_id') + if not all(isinstance(value, str) and value for value in ( + server_id, + item_id, + )): + return JSONResponse({'message': 'Select an item to retry.'}, status_code=400) + try: + result = mcp_tools.retry_items(server_id, [item_id]) + except mcp_tools.ToolInputError as error: + return JSONResponse({'message': str(error)}, status_code=400) + message = 'Retry queued. Follow progress in Activity.' if result['queued_item_ids'] \ + else 'This item is already queued.' + return JSONResponse({'message': message}, status_code=202) + + @router.api_route('/api/themerrdb', methods=['GET', 'HEAD'], name='server_ui.database_status', response_model=None) def database_status(): """Return the latest successful ThemerrDB Pages deployment from the hourly cache. diff --git a/src/themerr/mcp_tools.py b/src/themerr/mcp_tools.py index ac8b05da..2b4737d7 100644 --- a/src/themerr/mcp_tools.py +++ b/src/themerr/mcp_tools.py @@ -317,7 +317,7 @@ def retry_items(server_id, item_ids): queued.append(item_id) else: existing.append(item_id) - logger.get_logger(__name__).info('MCP retry requested for server %s: %d queued, %d already active', + logger.get_logger(__name__).info('Retry requested for server %s: %d queued, %d already active', record['id'], len(queued), len(existing)) return { 'queued_item_ids': queued, diff --git a/tests/functional/test_server_ui.py b/tests/functional/test_server_ui.py index dc279f23..3329a1c7 100644 --- a/tests/functional/test_server_ui.py +++ b/tests/functional/test_server_ui.py @@ -450,3 +450,94 @@ def test_contribution_actions_reflect_video_issues(client, reason, action, visib assert b'title="Movie" aria-hidden="true"' in rendered assert b'data-lucide="film"' in rendered assert b'data-lucide="play"' in rendered + + +def test_retry_button_only_shows_for_pending_and_attention_items(client): + statuses = { + 'a': ('themerr', 'complete'), + 'b': (None, 'pending'), + 'c': (None, 'failed'), + 'd': (None, 'missing'), + 'e': (None, 'unresolved'), + 'f': ('themerr', 'complete'), + } + for identifier, (provider, status) in statuses.items(): + save_server(identifier) + data = snapshot('Title ' + identifier, provider) + data['1']['items'][0]['theme_status'] = status + with storage.server_scope(identifier): + storage.replace_dashboard(data) + with storage.server_scope('f'): + storage.set_error(42, 'Theme is outdated') + page = client.get('/').text + shown = set(re.findall(r'data-retry-item data-server-id="([^"]+)" data-item-id="42"', page)) + assert shown == { + 'b', + 'c', + 'e', + 'f', + } + + +def test_retry_queues_a_cached_item(client, monkeypatch): + save_server('one') + with storage.server_scope('one'): + storage.replace_dashboard(snapshot('Pending')) + enqueue = Mock(return_value=True) + monkeypatch.setattr(processing, 'enqueue', enqueue) + response = client.post('/api/tasks/retry', json={'server_id': 'one', 'item_id': '42'}, headers=headers(client)) + assert response.status_code == 202 + assert response.json()['message'] == 'Retry queued. Follow progress in Activity.' + enqueue.assert_called_once_with('42') + + enqueue.return_value = False + response = client.post('/api/tasks/retry', json={'server_id': 'one', 'item_id': '42'}, headers=headers(client)) + assert response.status_code == 202 + assert response.json()['message'] == 'This item is already queued.' + + +@pytest.mark.parametrize('body', [ + [], + {}, + {'server_id': 'one'}, + {'server_id': 'one', 'item_id': ''}, + {'server_id': 'one', 'item_id': 42}, + {'server_id': 'missing', 'item_id': '42'}, + {'server_id': 'one', 'item_id': '99'}, +]) +def test_retry_rejects_invalid_or_unknown_items(client, monkeypatch, body): + save_server('one') + with storage.server_scope('one'): + storage.replace_dashboard(snapshot('Pending')) + enqueue = Mock(return_value=True) + monkeypatch.setattr(processing, 'enqueue', enqueue) + response = client.post('/api/tasks/retry', json=body, headers=headers(client)) + assert response.status_code == 400 + enqueue.assert_not_called() + + +def test_retry_respects_disabled_updates_servers_ignored_libraries_and_csrf(client, configured, monkeypatch): + save_server('one') + with storage.server_scope('one'): + storage.replace_dashboard(snapshot('Pending')) + enqueue = Mock(return_value=True) + monkeypatch.setattr(processing, 'enqueue', enqueue) + body = {'server_id': 'one', 'item_id': '42'} + assert client.post('/api/tasks/retry', json=body).status_code == 400 + + configured['Themerr']['BOOL_THEMERR_ENABLED'] = False + assert client.post('/api/tasks/retry', json=body, headers=headers(client)).status_code == 400 + configured['Themerr']['BOOL_THEMERR_ENABLED'] = True + + with Session(storage.engine()) as session: + session.get(servers.ServerRecord, 'one').ignored_libraries = '1' + session.commit() + assert client.post('/api/tasks/retry', json=body, headers=headers(client)).status_code == 400 + + with Session(storage.engine()) as session: + record = session.get(servers.ServerRecord, 'one') + record.ignored_libraries = '' + record.enabled = False + session.commit() + assert client.post('/api/tasks/retry', json=body, headers=headers(client)).status_code == 400 + enqueue.assert_not_called() diff --git a/tests/web/dashboard.test.mjs b/tests/web/dashboard.test.mjs index 8f86790c..9e2f8720 100644 --- a/tests/web/dashboard.test.mjs +++ b/tests/web/dashboard.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { matchesFilters, initDashboard, waitForRefresh } from '../../web/js/dashboard.js'; +import { matchesFilters, initDashboard, retryItem, waitForRefresh } from '../../web/js/dashboard.js'; test('library filters distinguish the same title on separate servers and handle Unicode search', () => { const media = { title: 'Léon: The Professional', server: 'a', type: 'movie', theme: true, attention: false }; @@ -65,3 +65,16 @@ test('refresh waits for its own job and reports failures or long-running work', assert.equal(await waitForRefresh('requested', async () => ({ jobs: [] }), async () => {}), 'running'); }); + +test('retry posts the selected server item and reports the result', async () => { + const calls = []; + const messages = []; + const button = { dataset: { serverId: 'server-1', itemId: '42' } }; + const result = await retryItem(button, async (path, options) => { + calls.push([path, options]); + return { message: 'Retry queued. Follow progress in Activity.' }; + }, message => messages.push(message)); + assert.deepEqual(calls, [['/api/tasks/retry', { body: { server_id: 'server-1', item_id: '42' } }]]); + assert.deepEqual(messages, ['Retry queued. Follow progress in Activity.']); + assert.equal(result.message, messages[0]); +}); diff --git a/web/css/custom.css b/web/css/custom.css index 10e11404..239f35ca 100644 --- a/web/css/custom.css +++ b/web/css/custom.css @@ -200,6 +200,10 @@ details[open] > summary .details-chevron { transform: rotate(180deg); } .failure-reason { margin-top: 7px; max-width: 330px; font-size: 10px; color: var(--danger); overflow-wrap: anywhere; } .failure-reason strong { display: flex; align-items: center; gap: 5px; margin-bottom: 4px; color: var(--warning); font-weight: 500; } .failure-reason .lucide { width: 12px; height: 12px; } +.retry-button { display: flex; align-items: center; gap: 5px; margin-top: 7px; padding: 3px 7px; background: transparent; color: var(--ink-soft); border: 1px solid var(--line); border-radius: 6px; font-size: 10px; } +.retry-button:hover:not(:disabled) { color: var(--accent); background: var(--surface-raised); } +.retry-button:disabled { opacity: .6; cursor: progress; } +.retry-button .lucide { width: 12px; height: 12px; } .contribute-link { display: inline-flex; align-items: center; gap: 5px; color: var(--ink-soft); font-size: 11px; } .contribute-link:hover { color: var(--accent); } .contribute-link .lucide { width: 13px; height: 13px; } diff --git a/web/js/dashboard.js b/web/js/dashboard.js index e86bdfe2..ef2598ce 100644 --- a/web/js/dashboard.js +++ b/web/js/dashboard.js @@ -68,6 +68,14 @@ export async function waitForRefresh(id, query, delay) { return 'running'; } +export async function retryItem(button, request = api, notify = toast) { + const result = await request('/api/tasks/retry', { + body: { server_id: button.dataset.serverId, item_id: button.dataset.itemId }, + }); + notify(result.message); + return result; +} + async function refreshLibraries(button) { const result = await api('/api/tasks/refresh', { body: { scan: button.dataset.scan === 'true' } }); toast(result.message); @@ -125,6 +133,8 @@ export function initNavigation() { export function initPageControls() { document.querySelectorAll('[data-refresh]').forEach(button => button.addEventListener('click', () => busy(button, () => refreshLibraries(button)))); + document.querySelectorAll('[data-retry-item]').forEach(button => button.addEventListener('click', + () => busy(button, () => retryItem(button)))); document.querySelectorAll('time').forEach(element => { const date = new Date(element.textContent); if (!Number.isNaN(date.valueOf())) { diff --git a/web/templates/home.html b/web/templates/home.html index 46f2a72b..f61ddb44 100644 --- a/web/templates/home.html +++ b/web/templates/home.html @@ -78,6 +78,7 @@

{{ _('Your library deserves a soundtrack.') if not servers else _('Getting t {% elif item.theme_status == 'unresolved' %}{{ _('TMDB ID unavailable') if item.source_id else _('External ID unavailable') }} {% else %}{{ _('Theme not installed yet') }}{% endif %} {% if failure_reason %}
{% if item.theme %}{{ _('Outdated from ThemerrDB') }}{% endif %}{{ failure_reason }}
{% endif %} + {% if (attention or item.theme_status == 'pending') and section.server_id != 'default' %}{% endif %} {% if item.issue_url and (item.issue_action != 'edit' or item.show_edit) %}{{ _('Edit') if item.issue_action == 'edit' else _('Add') }}{% else %}—{% endif %} From e7fb3dfc8e23205f538f44e8d3c97fb3c9c8b24d Mon Sep 17 00:00:00 2001 From: Andy Morrell Date: Fri, 30 Oct 2026 19:54:28 +0000 Subject: [PATCH 2/6] fix(web): include the visible Retry label in the button's accessible name --- locale/themerr.po | 4 ---- web/templates/home.html | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/locale/themerr.po b/locale/themerr.po index b75fd9ce..348317bb 100644 --- a/locale/themerr.po +++ b/locale/themerr.po @@ -1453,10 +1453,6 @@ msgstr "" msgid "Theme not installed yet" msgstr "" -#: web/templates/home.html:81 -msgid "Retry theme for" -msgstr "" - #: web/templates/home.html:81 msgid "Retry" msgstr "" diff --git a/web/templates/home.html b/web/templates/home.html index f61ddb44..8fb01d91 100644 --- a/web/templates/home.html +++ b/web/templates/home.html @@ -78,7 +78,7 @@

{{ _('Your library deserves a soundtrack.') if not servers else _('Getting t {% elif item.theme_status == 'unresolved' %}{{ _('TMDB ID unavailable') if item.source_id else _('External ID unavailable') }} {% else %}{{ _('Theme not installed yet') }}{% endif %} {% if failure_reason %}
{% if item.theme %}{{ _('Outdated from ThemerrDB') }}{% endif %}{{ failure_reason }}
{% endif %} - {% if (attention or item.theme_status == 'pending') and section.server_id != 'default' %}{% endif %} + {% if (attention or item.theme_status == 'pending') and section.server_id != 'default' %}{% endif %} {% if item.issue_url and (item.issue_action != 'edit' or item.show_edit) %}{{ _('Edit') if item.issue_action == 'edit' else _('Add') }}{% else %}—{% endif %} From cb59c1ed7a3e033d3c2ee78e7db0cf2cbed1094d Mon Sep 17 00:00:00 2001 From: Andy Morrell Date: Fri, 30 Oct 2026 19:57:22 +0000 Subject: [PATCH 3/6] ci: retrigger documentation build From 00c95779ab4e4f2b45a1ba6363661296f144a572 Mon Sep 17 00:00:00 2001 From: Andy Morrell Date: Fri, 30 Oct 2026 21:11:22 +0000 Subject: [PATCH 4/6] style(web): format retry additions and keep Retry beside its status badge --- tests/functional/test_server_ui.py | 65 +++++++++++++++++++++++------- tests/web/dashboard.test.mjs | 33 +++++++++++---- web/css/custom.css | 2 +- web/js/dashboard.js | 5 ++- web/templates/home.html | 2 +- 5 files changed, 82 insertions(+), 25 deletions(-) diff --git a/tests/functional/test_server_ui.py b/tests/functional/test_server_ui.py index 3329a1c7..4a17f6c4 100644 --- a/tests/functional/test_server_ui.py +++ b/tests/functional/test_server_ui.py @@ -452,18 +452,43 @@ def test_contribution_actions_reflect_video_issues(client, reason, action, visib assert b'data-lucide="play"' in rendered +def _retry_body(server_id='one', item_id='42'): + return { + 'server_id': server_id, + 'item_id': item_id, + } + + def test_retry_button_only_shows_for_pending_and_attention_items(client): statuses = { - 'a': ('themerr', 'complete'), - 'b': (None, 'pending'), - 'c': (None, 'failed'), - 'd': (None, 'missing'), - 'e': (None, 'unresolved'), - 'f': ('themerr', 'complete'), + 'a': ( + 'themerr', + 'complete', + ), + 'b': ( + None, + 'pending', + ), + 'c': ( + None, + 'failed', + ), + 'd': ( + None, + 'missing', + ), + 'e': ( + None, + 'unresolved', + ), + 'f': ( + 'themerr', + 'complete', + ), } for identifier, (provider, status) in statuses.items(): save_server(identifier) - data = snapshot('Title ' + identifier, provider) + data = snapshot(f'Title {identifier}', provider) data['1']['items'][0]['theme_status'] = status with storage.server_scope(identifier): storage.replace_dashboard(data) @@ -485,13 +510,21 @@ def test_retry_queues_a_cached_item(client, monkeypatch): storage.replace_dashboard(snapshot('Pending')) enqueue = Mock(return_value=True) monkeypatch.setattr(processing, 'enqueue', enqueue) - response = client.post('/api/tasks/retry', json={'server_id': 'one', 'item_id': '42'}, headers=headers(client)) + response = client.post( + '/api/tasks/retry', + json=_retry_body(), + headers=headers(client), + ) assert response.status_code == 202 assert response.json()['message'] == 'Retry queued. Follow progress in Activity.' enqueue.assert_called_once_with('42') enqueue.return_value = False - response = client.post('/api/tasks/retry', json={'server_id': 'one', 'item_id': '42'}, headers=headers(client)) + response = client.post( + '/api/tasks/retry', + json=_retry_body(), + headers=headers(client), + ) assert response.status_code == 202 assert response.json()['message'] == 'This item is already queued.' @@ -499,11 +532,13 @@ def test_retry_queues_a_cached_item(client, monkeypatch): @pytest.mark.parametrize('body', [ [], {}, - {'server_id': 'one'}, - {'server_id': 'one', 'item_id': ''}, - {'server_id': 'one', 'item_id': 42}, - {'server_id': 'missing', 'item_id': '42'}, - {'server_id': 'one', 'item_id': '99'}, + { + 'server_id': 'one', + }, + _retry_body(item_id=''), + _retry_body(item_id=42), + _retry_body(server_id='missing'), + _retry_body(item_id='99'), ]) def test_retry_rejects_invalid_or_unknown_items(client, monkeypatch, body): save_server('one') @@ -522,7 +557,7 @@ def test_retry_respects_disabled_updates_servers_ignored_libraries_and_csrf(clie storage.replace_dashboard(snapshot('Pending')) enqueue = Mock(return_value=True) monkeypatch.setattr(processing, 'enqueue', enqueue) - body = {'server_id': 'one', 'item_id': '42'} + body = _retry_body() assert client.post('/api/tasks/retry', json=body).status_code == 400 configured['Themerr']['BOOL_THEMERR_ENABLED'] = False diff --git a/tests/web/dashboard.test.mjs b/tests/web/dashboard.test.mjs index 9e2f8720..e5a6e20b 100644 --- a/tests/web/dashboard.test.mjs +++ b/tests/web/dashboard.test.mjs @@ -69,12 +69,31 @@ test('refresh waits for its own job and reports failures or long-running work', test('retry posts the selected server item and reports the result', async () => { const calls = []; const messages = []; - const button = { dataset: { serverId: 'server-1', itemId: '42' } }; + const message = 'Retry queued. Follow progress in Activity.'; + const button = { + dataset: { + serverId: 'server-1', + itemId: '42', + }, + }; const result = await retryItem(button, async (path, options) => { - calls.push([path, options]); - return { message: 'Retry queued. Follow progress in Activity.' }; - }, message => messages.push(message)); - assert.deepEqual(calls, [['/api/tasks/retry', { body: { server_id: 'server-1', item_id: '42' } }]]); - assert.deepEqual(messages, ['Retry queued. Follow progress in Activity.']); - assert.equal(result.message, messages[0]); + calls.push([ + path, + options, + ]); + return { message }; + }, text => messages.push(text)); + assert.deepEqual(calls, [ + [ + '/api/tasks/retry', + { + body: { + server_id: 'server-1', + item_id: '42', + }, + }, + ], + ]); + assert.deepEqual(messages, [message]); + assert.equal(result.message, message); }); diff --git a/web/css/custom.css b/web/css/custom.css index 239f35ca..f80385c0 100644 --- a/web/css/custom.css +++ b/web/css/custom.css @@ -200,7 +200,7 @@ details[open] > summary .details-chevron { transform: rotate(180deg); } .failure-reason { margin-top: 7px; max-width: 330px; font-size: 10px; color: var(--danger); overflow-wrap: anywhere; } .failure-reason strong { display: flex; align-items: center; gap: 5px; margin-bottom: 4px; color: var(--warning); font-weight: 500; } .failure-reason .lucide { width: 12px; height: 12px; } -.retry-button { display: flex; align-items: center; gap: 5px; margin-top: 7px; padding: 3px 7px; background: transparent; color: var(--ink-soft); border: 1px solid var(--line); border-radius: 6px; font-size: 10px; } +.retry-button { display: inline-flex; align-items: center; vertical-align: middle; gap: 5px; margin: 3px 0 3px 7px; padding: 3px 7px; background: transparent; color: var(--ink-soft); border: 1px solid var(--line); border-radius: 6px; font-size: 10px; white-space: nowrap; } .retry-button:hover:not(:disabled) { color: var(--accent); background: var(--surface-raised); } .retry-button:disabled { opacity: .6; cursor: progress; } .retry-button .lucide { width: 12px; height: 12px; } diff --git a/web/js/dashboard.js b/web/js/dashboard.js index ef2598ce..e3b04f17 100644 --- a/web/js/dashboard.js +++ b/web/js/dashboard.js @@ -70,7 +70,10 @@ export async function waitForRefresh(id, query, delay) { export async function retryItem(button, request = api, notify = toast) { const result = await request('/api/tasks/retry', { - body: { server_id: button.dataset.serverId, item_id: button.dataset.itemId }, + body: { + server_id: button.dataset.serverId, + item_id: button.dataset.itemId, + }, }); notify(result.message); return result; diff --git a/web/templates/home.html b/web/templates/home.html index 8fb01d91..c96129ba 100644 --- a/web/templates/home.html +++ b/web/templates/home.html @@ -77,8 +77,8 @@

{{ _('Your library deserves a soundtrack.') if not servers else _('Getting t {% elif item.theme_status == 'missing' %}{{ _('Missing from ThemerrDB') }} {% elif item.theme_status == 'unresolved' %}{{ _('TMDB ID unavailable') if item.source_id else _('External ID unavailable') }} {% else %}{{ _('Theme not installed yet') }}{% endif %} - {% if failure_reason %}
{% if item.theme %}{{ _('Outdated from ThemerrDB') }}{% endif %}{{ failure_reason }}
{% endif %} {% if (attention or item.theme_status == 'pending') and section.server_id != 'default' %}{% endif %} + {% if failure_reason %}
{% if item.theme %}{{ _('Outdated from ThemerrDB') }}{% endif %}{{ failure_reason }}
{% endif %} {% if item.issue_url and (item.issue_action != 'edit' or item.show_edit) %}{{ _('Edit') if item.issue_action == 'edit' else _('Add') }}{% else %}—{% endif %} From 37bfc67d9f3af26a761974e72b676b3c9338ae39 Mon Sep 17 00:00:00 2001 From: Andy Morrell Date: Fri, 30 Oct 2026 21:21:58 +0000 Subject: [PATCH 5/6] style(web): align the Retry button with its status badge --- web/css/custom.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/css/custom.css b/web/css/custom.css index f80385c0..ce807dd9 100644 --- a/web/css/custom.css +++ b/web/css/custom.css @@ -200,10 +200,10 @@ details[open] > summary .details-chevron { transform: rotate(180deg); } .failure-reason { margin-top: 7px; max-width: 330px; font-size: 10px; color: var(--danger); overflow-wrap: anywhere; } .failure-reason strong { display: flex; align-items: center; gap: 5px; margin-bottom: 4px; color: var(--warning); font-weight: 500; } .failure-reason .lucide { width: 12px; height: 12px; } -.retry-button { display: inline-flex; align-items: center; vertical-align: middle; gap: 5px; margin: 3px 0 3px 7px; padding: 3px 7px; background: transparent; color: var(--ink-soft); border: 1px solid var(--line); border-radius: 6px; font-size: 10px; white-space: nowrap; } +.retry-button { display: inline-flex; align-items: center; gap: 6px; margin: 3px 0 3px 7px; padding: 4px 7px; background: transparent; color: var(--ink-soft); border: 1px solid var(--line); border-radius: 6px; font-size: 10px; font-weight: 500; line-height: inherit; white-space: nowrap; } .retry-button:hover:not(:disabled) { color: var(--accent); background: var(--surface-raised); } .retry-button:disabled { opacity: .6; cursor: progress; } -.retry-button .lucide { width: 12px; height: 12px; } +.retry-button .lucide { width: 13px; height: 13px; stroke-width: 2; } .contribute-link { display: inline-flex; align-items: center; gap: 5px; color: var(--ink-soft); font-size: 11px; } .contribute-link:hover { color: var(--accent); } .contribute-link .lucide { width: 13px; height: 13px; } From bc01a42a3d3e5b4c0c7f4c2503f244cfaa994539 Mon Sep 17 00:00:00 2001 From: Andy Morrell Date: Sun, 1 Nov 2026 19:53:22 +0000 Subject: [PATCH 6/6] chore(locale): update PO template --- locale/themerr.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locale/themerr.po b/locale/themerr.po index 348317bb..aace9039 100644 --- a/locale/themerr.po +++ b/locale/themerr.po @@ -905,7 +905,7 @@ msgstr "" msgid "ID" msgstr "" -#: web/templates/activity.html:8 web/templates/home.html:80 +#: web/templates/activity.html:8 web/templates/home.html:81 msgid "Outdated from ThemerrDB" msgstr "" @@ -1453,7 +1453,7 @@ msgstr "" msgid "Theme not installed yet" msgstr "" -#: web/templates/home.html:81 +#: web/templates/home.html:80 msgid "Retry" msgstr ""