feat: store bookmark last-opened timestamps only in DB
This commit is contained in:
@@ -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' });
|
||||
|
||||
68
web/app.js
68
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';
|
||||
|
||||
Reference in New Issue
Block a user