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 MIN_SIMILAR_TEXT_LENGTH = 60;
|
||||||
const MAX_BOOKMARK_LABEL_LENGTH = 120;
|
const MAX_BOOKMARK_LABEL_LENGTH = 120;
|
||||||
const MAX_BOOKMARK_QUERY_LENGTH = 200;
|
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_TITLE_MAX_LENGTH = 160;
|
||||||
const DAILY_BOOKMARK_URL_MAX_LENGTH = 800;
|
const DAILY_BOOKMARK_URL_MAX_LENGTH = 800;
|
||||||
const DAILY_BOOKMARK_NOTES_MAX_LENGTH = 800;
|
const DAILY_BOOKMARK_NOTES_MAX_LENGTH = 800;
|
||||||
@@ -917,18 +920,26 @@ function normalizeBookmarkLabel(value, fallback = '') {
|
|||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDefaultBookmarkQuery(value) {
|
||||||
|
return typeof value === 'string' && !value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
function serializeBookmark(row) {
|
function serializeBookmark(row) {
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDefault = isDefaultBookmarkQuery(row.query);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
label: row.label,
|
label: row.label,
|
||||||
query: row.query,
|
query: row.query,
|
||||||
created_at: sqliteTimestampToUTC(row.created_at),
|
created_at: sqliteTimestampToUTC(row.created_at),
|
||||||
updated_at: sqliteTimestampToUTC(row.updated_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 (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const ensureDefaultBookmarkStmt = db.prepare(`
|
||||||
|
INSERT OR IGNORE INTO bookmarks (id, label, query)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
`);
|
||||||
|
|
||||||
const deleteBookmarkStmt = db.prepare(`
|
const deleteBookmarkStmt = db.prepare(`
|
||||||
DELETE FROM bookmarks
|
DELETE FROM bookmarks
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
@@ -1669,6 +1685,8 @@ const updateBookmarkLastClickedStmt = db.prepare(`
|
|||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
ensureDefaultBookmarkStmt.run(DEFAULT_BOOKMARK_ID, DEFAULT_BOOKMARK_LABEL, DEFAULT_BOOKMARK_QUERY);
|
||||||
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS daily_bookmarks (
|
CREATE TABLE IF NOT EXISTS daily_bookmarks (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -4555,6 +4573,14 @@ app.delete('/api/bookmarks/:bookmarkId', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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);
|
const result = deleteBookmarkStmt.run(bookmarkId);
|
||||||
if (!result.changes) {
|
if (!result.changes) {
|
||||||
return res.status(404).json({ error: 'Bookmark nicht gefunden' });
|
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 BOOKMARKS_BASE_URL = 'https://www.facebook.com/search/top';
|
||||||
const BOOKMARK_WINDOW_DAYS = 28;
|
const BOOKMARK_WINDOW_DAYS = 28;
|
||||||
const BOOKMARK_SUFFIXES = ['Gewinnspiel', 'gewinnen', 'verlosen'];
|
const BOOKMARK_SUFFIXES = ['Gewinnspiel', 'gewinnen', 'verlosen'];
|
||||||
|
const DEFAULT_BOOKMARK_LABEL = 'Gewinnspiel / gewinnen / verlosen';
|
||||||
const BOOKMARK_PREFS_KEY = 'trackerBookmarkPreferences';
|
const BOOKMARK_PREFS_KEY = 'trackerBookmarkPreferences';
|
||||||
const INCLUDE_EXPIRED_STORAGE_KEY = 'trackerIncludeExpired';
|
const INCLUDE_EXPIRED_STORAGE_KEY = 'trackerIncludeExpired';
|
||||||
const PENDING_BULK_COUNT_STORAGE_KEY = 'trackerPendingBulkCount';
|
const PENDING_BULK_COUNT_STORAGE_KEY = 'trackerPendingBulkCount';
|
||||||
@@ -914,11 +915,14 @@ function normalizeServerBookmark(entry) {
|
|||||||
|
|
||||||
const id = typeof entry.id === 'string' ? entry.id : null;
|
const id = typeof entry.id === 'string' ? entry.id : null;
|
||||||
const query = typeof entry.query === 'string' ? entry.query.trim() : '';
|
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;
|
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) => {
|
const normalizeDate = (value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -938,7 +942,8 @@ function normalizeServerBookmark(entry) {
|
|||||||
created_at: normalizeDate(entry.created_at),
|
created_at: normalizeDate(entry.created_at),
|
||||||
updated_at: normalizeDate(entry.updated_at),
|
updated_at: normalizeDate(entry.updated_at),
|
||||||
last_clicked_at: normalizeDate(entry.last_clicked_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 = [];
|
const deduped = [];
|
||||||
|
|
||||||
list.forEach((bookmark) => {
|
list.forEach((bookmark) => {
|
||||||
if (!bookmark || !bookmark.query) {
|
if (!bookmark || typeof bookmark.id !== 'string') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = bookmark.query.toLowerCase();
|
const query = typeof bookmark.query === 'string' ? bookmark.query : '';
|
||||||
|
const key = query.toLowerCase();
|
||||||
if (seen.has(key)) {
|
if (seen.has(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1050,25 +1056,13 @@ function updateBookmarkSortDirectionUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_BOOKMARK_LAST_CLICK_KEY = 'trackerDefaultBookmarkLastClickedAt';
|
|
||||||
|
|
||||||
const bookmarkState = {
|
const bookmarkState = {
|
||||||
items: [],
|
items: [],
|
||||||
loaded: false,
|
loaded: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null
|
||||||
defaultLastClickedAt: 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;
|
let bookmarkFetchPromise = null;
|
||||||
|
|
||||||
function formatRelativeTimeFromNow(timestamp) {
|
function formatRelativeTimeFromNow(timestamp) {
|
||||||
@@ -1117,10 +1111,11 @@ function upsertBookmarkInState(bookmark) {
|
|||||||
|
|
||||||
const lowerQuery = normalized.query.toLowerCase();
|
const lowerQuery = normalized.query.toLowerCase();
|
||||||
const existingIndex = bookmarkState.items.findIndex((item) => {
|
const existingIndex = bookmarkState.items.findIndex((item) => {
|
||||||
if (!item || !item.query) {
|
if (!item || typeof item.id !== 'string') {
|
||||||
return false;
|
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) {
|
if (existingIndex >= 0) {
|
||||||
@@ -1419,26 +1414,18 @@ function openBookmark(bookmark) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stateBookmark = bookmarkState.items.find((item) => item.id === bookmark.id) || bookmark;
|
const stateBookmark = bookmarkState.items.find((item) => item.id === bookmark.id) || bookmark;
|
||||||
const isDefaultBookmark = stateBookmark && stateBookmark.isDefault;
|
|
||||||
|
|
||||||
const nowIso = new Date().toISOString();
|
const nowIso = new Date().toISOString();
|
||||||
|
|
||||||
if (isDefaultBookmark) {
|
if (stateBookmark && stateBookmark.id) {
|
||||||
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) {
|
|
||||||
upsertBookmarkInState({
|
upsertBookmarkInState({
|
||||||
id: stateBookmark.id,
|
id: stateBookmark.id,
|
||||||
label: stateBookmark.label,
|
label: stateBookmark.label,
|
||||||
query: stateBookmark.query,
|
query: stateBookmark.query,
|
||||||
last_clicked_at: nowIso,
|
last_clicked_at: nowIso,
|
||||||
created_at: stateBookmark.created_at || nowIso,
|
created_at: stateBookmark.created_at || nowIso,
|
||||||
updated_at: nowIso
|
updated_at: nowIso,
|
||||||
|
deletable: stateBookmark.deletable !== false,
|
||||||
|
is_default: stateBookmark.isDefault === true
|
||||||
});
|
});
|
||||||
renderBookmarks();
|
renderBookmarks();
|
||||||
markBookmarkClick(stateBookmark.id);
|
markBookmarkClick(stateBookmark.id);
|
||||||
@@ -1588,20 +1575,13 @@ function renderBookmarks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dynamicBookmarks = bookmarkState.items;
|
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 filteredBookmarks = filterBookmarksBySearch(dynamicBookmarks);
|
||||||
const sortedForAll = sortBookmarksForDisplay(filteredBookmarks);
|
const sortedForAll = sortBookmarksForDisplay(filteredBookmarks);
|
||||||
|
const defaultBookmark = sortedForAll.find((bookmark) => bookmark.isDefault);
|
||||||
const displayList = bookmarkSearchTerm ? sortedForAll : [staticDefault, ...sortedForAll];
|
const nonDefaultBookmarks = sortedForAll.filter((bookmark) => !bookmark.isDefault);
|
||||||
|
const displayList = bookmarkSearchTerm
|
||||||
|
? sortedForAll
|
||||||
|
: (defaultBookmark ? [defaultBookmark, ...nonDefaultBookmarks] : nonDefaultBookmarks);
|
||||||
const titleText = bookmarkSearchTerm
|
const titleText = bookmarkSearchTerm
|
||||||
? (filteredBookmarks.length ? `Suchergebnisse (${filteredBookmarks.length})` : 'Keine Treffer')
|
? (filteredBookmarks.length ? `Suchergebnisse (${filteredBookmarks.length})` : 'Keine Treffer')
|
||||||
: 'Alle Bookmarks';
|
: 'Alle Bookmarks';
|
||||||
|
|||||||
Reference in New Issue
Block a user