diff --git a/backend/server.js b/backend/server.js index c63b5fb..0b3d6f8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -28,6 +28,9 @@ const MIN_TEXT_HASH_LENGTH = 120; const MIN_SIMILAR_TEXT_LENGTH = 60; const MAX_BOOKMARK_LABEL_LENGTH = 120; const MAX_BOOKMARK_QUERY_LENGTH = 200; +const DEFAULT_BOOKMARK_ID = 'default-search'; +const DEFAULT_BOOKMARK_LABEL = 'Gewinnspiel / gewinnen / verlosen'; +const DEFAULT_BOOKMARK_QUERY = ''; const DAILY_BOOKMARK_TITLE_MAX_LENGTH = 160; const DAILY_BOOKMARK_URL_MAX_LENGTH = 800; const DAILY_BOOKMARK_NOTES_MAX_LENGTH = 800; @@ -917,18 +920,26 @@ function normalizeBookmarkLabel(value, fallback = '') { return label; } +function isDefaultBookmarkQuery(value) { + return typeof value === 'string' && !value.trim(); +} + function serializeBookmark(row) { if (!row) { return null; } + const isDefault = isDefaultBookmarkQuery(row.query); + return { id: row.id, label: row.label, query: row.query, created_at: sqliteTimestampToUTC(row.created_at), updated_at: sqliteTimestampToUTC(row.updated_at), - last_clicked_at: sqliteTimestampToUTC(row.last_clicked_at) + last_clicked_at: sqliteTimestampToUTC(row.last_clicked_at), + is_default: isDefault, + deletable: !isDefault }; } @@ -1657,6 +1668,11 @@ const insertBookmarkStmt = db.prepare(` VALUES (?, ?, ?) `); +const ensureDefaultBookmarkStmt = db.prepare(` + INSERT OR IGNORE INTO bookmarks (id, label, query) + VALUES (?, ?, ?) +`); + const deleteBookmarkStmt = db.prepare(` DELETE FROM bookmarks WHERE id = ? @@ -1669,6 +1685,8 @@ const updateBookmarkLastClickedStmt = db.prepare(` WHERE id = ? `); +ensureDefaultBookmarkStmt.run(DEFAULT_BOOKMARK_ID, DEFAULT_BOOKMARK_LABEL, DEFAULT_BOOKMARK_QUERY); + db.exec(` CREATE TABLE IF NOT EXISTS daily_bookmarks ( id TEXT PRIMARY KEY, @@ -4555,6 +4573,14 @@ app.delete('/api/bookmarks/:bookmarkId', (req, res) => { } try { + const existing = getBookmarkByIdStmt.get(bookmarkId); + if (!existing) { + return res.status(404).json({ error: 'Bookmark nicht gefunden' }); + } + if (isDefaultBookmarkQuery(existing.query)) { + return res.status(403).json({ error: 'Standard-Bookmark kann nicht gelöscht werden' }); + } + const result = deleteBookmarkStmt.run(bookmarkId); if (!result.changes) { return res.status(404).json({ error: 'Bookmark nicht gefunden' }); diff --git a/web/app.js b/web/app.js index a909f38..ea12b2c 100644 --- a/web/app.js +++ b/web/app.js @@ -379,6 +379,7 @@ const DEFAULT_SORT_SETTINGS = { mode: 'created', direction: 'desc' }; const BOOKMARKS_BASE_URL = 'https://www.facebook.com/search/top'; const BOOKMARK_WINDOW_DAYS = 28; const BOOKMARK_SUFFIXES = ['Gewinnspiel', 'gewinnen', 'verlosen']; +const DEFAULT_BOOKMARK_LABEL = 'Gewinnspiel / gewinnen / verlosen'; const BOOKMARK_PREFS_KEY = 'trackerBookmarkPreferences'; const INCLUDE_EXPIRED_STORAGE_KEY = 'trackerIncludeExpired'; const PENDING_BULK_COUNT_STORAGE_KEY = 'trackerPendingBulkCount'; @@ -914,11 +915,14 @@ function normalizeServerBookmark(entry) { const id = typeof entry.id === 'string' ? entry.id : null; const query = typeof entry.query === 'string' ? entry.query.trim() : ''; - if (!id || !query) { + const isDefault = entry.is_default === true || !query; + if (!id || (!isDefault && !query)) { return null; } - const label = typeof entry.label === 'string' && entry.label.trim() ? entry.label.trim() : query; + const label = typeof entry.label === 'string' && entry.label.trim() + ? entry.label.trim() + : (isDefault ? DEFAULT_BOOKMARK_LABEL : query); const normalizeDate = (value) => { if (!value) { @@ -938,7 +942,8 @@ function normalizeServerBookmark(entry) { created_at: normalizeDate(entry.created_at), updated_at: normalizeDate(entry.updated_at), last_clicked_at: normalizeDate(entry.last_clicked_at), - deletable: true + deletable: entry.deletable === false ? false : !isDefault, + isDefault }; } @@ -947,10 +952,11 @@ function deduplicateBookmarks(list) { const deduped = []; list.forEach((bookmark) => { - if (!bookmark || !bookmark.query) { + if (!bookmark || typeof bookmark.id !== 'string') { return; } - const key = bookmark.query.toLowerCase(); + const query = typeof bookmark.query === 'string' ? bookmark.query : ''; + const key = query.toLowerCase(); if (seen.has(key)) { return; } @@ -1050,25 +1056,13 @@ function updateBookmarkSortDirectionUI() { } } -const DEFAULT_BOOKMARK_LAST_CLICK_KEY = 'trackerDefaultBookmarkLastClickedAt'; - const bookmarkState = { items: [], loaded: false, loading: false, - error: null, - defaultLastClickedAt: null + error: null }; -try { - const storedDefaultBookmark = localStorage.getItem(DEFAULT_BOOKMARK_LAST_CLICK_KEY); - if (storedDefaultBookmark) { - bookmarkState.defaultLastClickedAt = storedDefaultBookmark; - } -} catch (error) { - console.warn('Konnte letzte Nutzung des Standard-Bookmarks nicht laden:', error); -} - let bookmarkFetchPromise = null; function formatRelativeTimeFromNow(timestamp) { @@ -1117,10 +1111,11 @@ function upsertBookmarkInState(bookmark) { const lowerQuery = normalized.query.toLowerCase(); const existingIndex = bookmarkState.items.findIndex((item) => { - if (!item || !item.query) { + if (!item || typeof item.id !== 'string') { return false; } - return item.id === normalized.id || item.query.toLowerCase() === lowerQuery; + const itemQuery = typeof item.query === 'string' ? item.query.toLowerCase() : ''; + return item.id === normalized.id || itemQuery === lowerQuery; }); if (existingIndex >= 0) { @@ -1419,26 +1414,18 @@ function openBookmark(bookmark) { } const stateBookmark = bookmarkState.items.find((item) => item.id === bookmark.id) || bookmark; - const isDefaultBookmark = stateBookmark && stateBookmark.isDefault; - const nowIso = new Date().toISOString(); - if (isDefaultBookmark) { - bookmarkState.defaultLastClickedAt = nowIso; - try { - localStorage.setItem(DEFAULT_BOOKMARK_LAST_CLICK_KEY, nowIso); - } catch (error) { - console.warn('Konnte Standard-Bookmark-Zeit nicht speichern:', error); - } - renderBookmarks(); - } else if (stateBookmark && stateBookmark.id && stateBookmark.deletable !== false) { + if (stateBookmark && stateBookmark.id) { upsertBookmarkInState({ id: stateBookmark.id, label: stateBookmark.label, query: stateBookmark.query, last_clicked_at: nowIso, created_at: stateBookmark.created_at || nowIso, - updated_at: nowIso + updated_at: nowIso, + deletable: stateBookmark.deletable !== false, + is_default: stateBookmark.isDefault === true }); renderBookmarks(); markBookmarkClick(stateBookmark.id); @@ -1588,20 +1575,13 @@ function renderBookmarks() { } const dynamicBookmarks = bookmarkState.items; - - const staticDefault = { - id: 'default-search', - label: 'Gewinnspiel / gewinnen / verlosen', - query: '', - last_clicked_at: bookmarkState.defaultLastClickedAt || null, - deletable: false, - isDefault: true - }; - const filteredBookmarks = filterBookmarksBySearch(dynamicBookmarks); const sortedForAll = sortBookmarksForDisplay(filteredBookmarks); - - const displayList = bookmarkSearchTerm ? sortedForAll : [staticDefault, ...sortedForAll]; + const defaultBookmark = sortedForAll.find((bookmark) => bookmark.isDefault); + const nonDefaultBookmarks = sortedForAll.filter((bookmark) => !bookmark.isDefault); + const displayList = bookmarkSearchTerm + ? sortedForAll + : (defaultBookmark ? [defaultBookmark, ...nonDefaultBookmarks] : nonDefaultBookmarks); const titleText = bookmarkSearchTerm ? (filteredBookmarks.length ? `Suchergebnisse (${filteredBookmarks.length})` : 'Keine Treffer') : 'Alle Bookmarks';