feat: store bookmark last-opened timestamps only in DB

This commit is contained in:
2026-02-12 17:57:21 +01:00
parent bbfa93a586
commit 2feba4e585
2 changed files with 51 additions and 45 deletions

View File

@@ -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' });

View File

@@ -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';