diff --git a/VIBE/CHAT_LOG.md b/VIBE/CHAT_LOG.md index a5de1e2..8624c98 100644 --- a/VIBE/CHAT_LOG.md +++ b/VIBE/CHAT_LOG.md @@ -640,3 +640,15 @@ Only print the `from` line if there is no title. Put the title directly behind t ### Assistant outcome Updated Mastodon formatting to place the title directly after `From my #LinkLog:`, omit the `from:` line when no title is available, and place tags on a separate final line. Added no-title regression coverage. The focused tests passed and the complete Dockerized backend suite passed with 23 tests. + +### User +Do not show edit buttons on the home page. On `//`, show the edit button on the right and add a delete button. + +### Assistant outcome +Added an owner-scoped `DELETE /api/links/{link_id}` endpoint. Feed edit/delete controls now render only on user-specific pages, never on the home feed, and are right-aligned in an entry action group. Added owner-only deletion regression coverage and updated the feed cache key. The complete Dockerized backend suite passed with 23 tests, and the running container is healthy. + +### User +Put the buttons on the right-hand side of the link item, make them smaller, and keep the same color scheme. + +### Assistant outcome +Changed the entry action group to float at the right edge of user-specific link items, allowing content to wrap around it. Reduced Edit and Delete button padding, radius, and font size while preserving Catppuccin surface and danger colors. diff --git a/VIBE/PROMPTS.md b/VIBE/PROMPTS.md index 1c84f37..d8b376a 100644 --- a/VIBE/PROMPTS.md +++ b/VIBE/PROMPTS.md @@ -117,6 +117,8 @@ 111. LINKLOG_PUBLIC_URL and TRAEFIK_HOST are the same and can be merged. (use LINKLOG_PUBLIC_URL), fix docker-compose to use said variable 112. Format mastodon posts like From my #LinkLog: Title, optional comment, and from: URL 113. Only print the 'from' line if there is no title. Put the title directly behind the colon, and put all tags on the last line. +114. In the frontend do not show edit buttons on the home page even when a user is logged in. On the // page show the edit button on the right of the entry. Also add a delete button. +115. Put the buttons on the right hand side of the link item, make the buttons smaller and same color scheme ## Future entries diff --git a/backend/app/api/links.py b/backend/app/api/links.py index db0507a..2867fb3 100644 --- a/backend/app/api/links.py +++ b/backend/app/api/links.py @@ -5,7 +5,7 @@ from fastapi import APIRouter, Header, HTTPException, status import logging from pydantic import BaseModel -from backend.app.services.link_service import create_link, list_public_links, list_tags, update_link +from backend.app.services.link_service import create_link, delete_link, list_public_links, list_tags, update_link from backend.app.services.plugin_manager import plugin_manager from backend.app.services.token_service import validate_token @@ -73,6 +73,21 @@ def update_link_endpoint( return record +@router.delete('/links/{link_id}') +def delete_link_endpoint( + link_id: str, + authorization: str | None = Header(default=None), +): + if not authorization or not authorization.startswith('Bearer '): + raise HTTPException(status_code=401, detail='Missing or invalid Authorization header') + info = validate_token(authorization.replace('Bearer ', '', 1)) + if info is None: + raise HTTPException(status_code=401, detail='Token expired or invalid') + if not delete_link(link_id, info['user_id']): + raise HTTPException(status_code=404, detail='Link not found or not owned by user') + return {'status': 'deleted', 'id': link_id} + + @router.get('/links') def list_links(): return list_public_links() diff --git a/backend/app/services/link_service.py b/backend/app/services/link_service.py index c2bd0a8..ec677f0 100644 --- a/backend/app/services/link_service.py +++ b/backend/app/services/link_service.py @@ -157,6 +157,16 @@ def update_link( return record +def delete_link(link_id: str, user_id: str) -> bool: + with get_connection() as conn: + cursor = conn.execute( + 'DELETE FROM links WHERE id = ? AND user_id = ?', + (link_id, user_id), + ) + conn.commit() + return cursor.rowcount > 0 + + def list_public_users(): with get_connection() as conn: rows = conn.execute( diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 5489c48..1283a8f 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -334,6 +334,11 @@ def test_only_link_owner_can_edit_link(): }) assert denied.status_code == 404 + deleted = client.delete(f'/api/links/{link_id}', headers=owner_headers) + assert deleted.status_code == 200 + assert client.delete(f'/api/links/{link_id}', headers=owner_headers).status_code == 404 + assert client.delete(f'/api/links/{link_id}', headers=login_headers('bob')).status_code == 404 + def test_logout_revokes_token_and_admin_can_list_plugins(): headers = login_headers() @@ -396,7 +401,8 @@ def test_public_and_admin_pages_render_html(): assert '' in admin_page assert 'admin.js?v=5' in admin_page feed_script = client.get('/static/feed.js?v=7').text - assert 'if (item.is_owner)' in feed_script + assert 'if (item.is_owner && !showIdentity)' in feed_script + assert 'deleteEntry(item, deleteButton)' in feed_script assert 'tag.toLowerCase() === pref.tag.toLowerCase()' in feed_script assert 'return `${date.getFullYear()} ${months[date.getMonth()]} ${date.getDate()} - ${hours}:${minutes}`' in feed_script assert 'edit-tag-options' in feed_script diff --git a/frontend/static/feed.js b/frontend/static/feed.js index 3fe7828..41c0697 100644 --- a/frontend/static/feed.js +++ b/frontend/static/feed.js @@ -117,13 +117,21 @@ function renderFeed(items, showIdentity = true) { meta.className = 'meta'; meta.textContent = formatDate(item.created_at); - if (item.is_owner) { + if (item.is_owner && !showIdentity) { + const actions = document.createElement('div'); + actions.className = 'entry-actions'; const editButton = document.createElement('button'); editButton.type = 'button'; editButton.className = 'edit-button'; editButton.textContent = 'Edit'; editButton.addEventListener('click', () => showEditForm(article, item)); - article.appendChild(editButton); + const deleteButton = document.createElement('button'); + deleteButton.type = 'button'; + deleteButton.className = 'delete-button'; + deleteButton.textContent = 'Delete'; + deleteButton.addEventListener('click', () => deleteEntry(item, deleteButton)); + actions.append(editButton, deleteButton); + article.appendChild(actions); } if (showIdentity) { @@ -144,6 +152,20 @@ function renderFeed(items, showIdentity = true) { }); } +async function deleteEntry(item, button) { + if (!window.confirm(`Delete this link?`)) return; + button.disabled = true; + const response = await fetch(`/api/links/${encodeURIComponent(item.id)}`, { + method: 'DELETE', + headers: {Authorization: `Bearer ${accessToken}`}, + }); + if (response.ok) { + await loadFeed(); + } else { + button.disabled = false; + } +} + function showEditForm(article, item) { if (article.querySelector('.edit-form')) return; const form = document.createElement('form'); diff --git a/frontend/static/style.css b/frontend/static/style.css index 562aba6..7cd5eaf 100644 --- a/frontend/static/style.css +++ b/frontend/static/style.css @@ -420,11 +420,29 @@ button:disabled { .edit-button { min-width: 0; - margin-top: 14px; - padding: 8px 12px; + padding: 5px 9px; + border-radius: 6px; background: var(--surface-1); border-color: var(--surface-2); color: var(--text); + font-size: 0.82rem; +} + +.entry-actions { + display: flex; + float: right; + gap: 8px; + margin: 0 0 8px 16px; +} + +.delete-button { + min-width: 0; + padding: 5px 9px; + border-radius: 6px; + border-color: rgba(243, 139, 168, 0.45); + background: transparent; + color: var(--red); + font-size: 0.82rem; } .edit-form { diff --git a/frontend/templates/feed.html b/frontend/templates/feed.html index 53b509c..0cce94b 100644 --- a/frontend/templates/feed.html +++ b/frontend/templates/feed.html @@ -79,6 +79,6 @@ - + diff --git a/linklog_data/avatars/4f2eb936-8d54-4026-8a68-b3bfdfb5a8f9.jpg b/linklog_data/avatars/4f2eb936-8d54-4026-8a68-b3bfdfb5a8f9.jpg new file mode 100644 index 0000000..01154be Binary files /dev/null and b/linklog_data/avatars/4f2eb936-8d54-4026-8a68-b3bfdfb5a8f9.jpg differ