fix(trash): Daten-Review-Befunde — Import-Cap nach deletedAt sortiert (Verlust-Schutz), Restore-Doppelklick-Guard, Delete-Rollback bei Save-Fehler, NaN/Null-Haertung
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ async function init() {
|
|||||||
// zurueck, wenn wirklich etwas entfernt wurde (kein unnoetiger Storage-Write).
|
// zurueck, wenn wirklich etwas entfernt wurde (kein unnoetiger Storage-Write).
|
||||||
const cutoff = Date.now() - TRASH_RETENTION_MS;
|
const cutoff = Date.now() - TRASH_RETENTION_MS;
|
||||||
const beforeCount = trash.length;
|
const beforeCount = trash.length;
|
||||||
trash = trash.filter(entry => typeof entry.deletedAt === 'number' && entry.deletedAt >= cutoff);
|
trash = trash.filter(entry => entry && typeof entry.deletedAt === 'number' && Number.isFinite(entry.deletedAt) && entry.deletedAt >= cutoff);
|
||||||
if (trash.length !== beforeCount) await saveTrash();
|
if (trash.length !== beforeCount) await saveTrash();
|
||||||
if (savedSettings) Object.assign(settings, savedSettings);
|
if (savedSettings) Object.assign(settings, savedSettings);
|
||||||
|
|
||||||
|
|||||||
+17
-2
@@ -162,10 +162,18 @@ function createBoardEl(board) {
|
|||||||
// type:'board', kein originBoardId (Board hat keine Herkunft, Restore legt es direkt in boards[]).
|
// type:'board', kein originBoardId (Board hat keine Herkunft, Restore legt es direkt in boards[]).
|
||||||
// Datensicherheit: ZUERST Trash sichern (saveTrash), DANN Loeschung committen (saveBoards) —
|
// Datensicherheit: ZUERST Trash sichern (saveTrash), DANN Loeschung committen (saveBoards) —
|
||||||
// bei Quota-Reject bleibt das Board in boards[], kein Datenverlust.
|
// bei Quota-Reject bleibt das Board in boards[], kein Datenverlust.
|
||||||
pushToTrash({ item: board, type: 'board', originBoardId: null });
|
const trashEntry = pushToTrash({ item: board, type: 'board', originBoardId: null });
|
||||||
|
try {
|
||||||
await saveTrash();
|
await saveTrash();
|
||||||
boards = boards.filter(b => b.id !== board.id);
|
boards = boards.filter(b => b.id !== board.id);
|
||||||
await saveBoards();
|
await saveBoards();
|
||||||
|
} catch (err) {
|
||||||
|
// Save fehlgeschlagen (z.B. Quota genau zwischen den Writes): auf den Vor-Loesch-Stand
|
||||||
|
// zurueckrollen, damit In-Memory und Storage konsistent bleiben (kein Reload-Duplikat).
|
||||||
|
trash = trash.filter(t => t !== trashEntry);
|
||||||
|
if (!boards.some(b => b.id === board.id)) boards.push(board);
|
||||||
|
console.error('Board-Loeschen fehlgeschlagen, zurueckgerollt:', err && err.message);
|
||||||
|
}
|
||||||
renderBoards();
|
renderBoards();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -277,10 +285,17 @@ function bindBoardListEvents(list, board) {
|
|||||||
if (removed) {
|
if (removed) {
|
||||||
// Datensicherheit: ZUERST den Trash-Klon persistieren, DANN die Loeschung committen.
|
// Datensicherheit: ZUERST den Trash-Klon persistieren, DANN die Loeschung committen.
|
||||||
// Falls saveTrash() (Quota) rejectet, ist das Original noch in boards[] -> kein Verlust.
|
// Falls saveTrash() (Quota) rejectet, ist das Original noch in boards[] -> kein Verlust.
|
||||||
pushToTrash({ item: removed, type: 'bookmark', originBoardId: board.id });
|
const trashEntry = pushToTrash({ item: removed, type: 'bookmark', originBoardId: board.id });
|
||||||
|
try {
|
||||||
await saveTrash();
|
await saveTrash();
|
||||||
board.bookmarks = board.bookmarks.filter(b => b.id !== bmId);
|
board.bookmarks = board.bookmarks.filter(b => b.id !== bmId);
|
||||||
await saveBoards();
|
await saveBoards();
|
||||||
|
} catch (err) {
|
||||||
|
// Save fehlgeschlagen: auf den Vor-Loesch-Stand zurueckrollen (kein Reload-Duplikat).
|
||||||
|
trash = trash.filter(t => t !== trashEntry);
|
||||||
|
if (!board.bookmarks.some(b => b.id === bmId)) board.bookmarks.push(removed);
|
||||||
|
console.error('Bookmark-Loeschen fehlgeschlagen, zurueckgerollt:', err && err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
renderBoards();
|
renderBoards();
|
||||||
return;
|
return;
|
||||||
|
|||||||
+8
-2
@@ -82,7 +82,7 @@ function initDataButtons() {
|
|||||||
// Papierkorb importieren (falls vorhanden) — defensiv validiert.
|
// Papierkorb importieren (falls vorhanden) — defensiv validiert.
|
||||||
if (Array.isArray(data.trash) && data.trash.length > 0) {
|
if (Array.isArray(data.trash) && data.trash.length > 0) {
|
||||||
const validTrash = data.trash
|
const validTrash = data.trash
|
||||||
.filter(e => e && e.item && ['bookmark', 'board'].includes(e.type) && typeof e.deletedAt === 'number')
|
.filter(e => e && e.item && ['bookmark', 'board'].includes(e.type) && typeof e.deletedAt === 'number' && Number.isFinite(e.deletedAt))
|
||||||
.map(e => ({
|
.map(e => ({
|
||||||
type: e.type,
|
type: e.type,
|
||||||
originBoardId: typeof e.originBoardId === 'string' ? e.originBoardId : null,
|
originBoardId: typeof e.originBoardId === 'string' ? e.originBoardId : null,
|
||||||
@@ -96,6 +96,7 @@ function initDataButtons() {
|
|||||||
? e.item.bookmarks
|
? e.item.bookmarks
|
||||||
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
|
.filter(bm => bm && typeof bm.title === 'string' && isSafeUrl(bm.url))
|
||||||
.map(bm => ({ id: bm.id || uid(), title: String(bm.title).slice(0, 200), url: bm.url, desc: String(bm.desc || '').slice(0, 500) }))
|
.map(bm => ({ id: bm.id || uid(), title: String(bm.title).slice(0, 200), url: bm.url, desc: String(bm.desc || '').slice(0, 500) }))
|
||||||
|
.slice(0, 500)
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
: (isSafeUrl(e.item.url)
|
: (isSafeUrl(e.item.url)
|
||||||
@@ -104,7 +105,12 @@ function initDataButtons() {
|
|||||||
}))
|
}))
|
||||||
.filter(e => e.item !== null);
|
.filter(e => e.item !== null);
|
||||||
if (validTrash.length > 0) {
|
if (validTrash.length > 0) {
|
||||||
trash = [...trash, ...validTrash].slice(-TRASH_MAX_ENTRIES);
|
// Nach deletedAt aufsteigend sortieren, DANN die neuesten TRASH_MAX_ENTRIES behalten.
|
||||||
|
// Positionsbasiertes slice(-N) wuerde sonst frische lokale Eintraege verdraengen
|
||||||
|
// statt der aeltesten — Datenverlust, da ein Trash-Eintrag die einzige Kopie ist.
|
||||||
|
const combined = [...trash, ...validTrash];
|
||||||
|
combined.sort((a, b) => a.deletedAt - b.deletedAt);
|
||||||
|
trash = combined.slice(-TRASH_MAX_ENTRIES);
|
||||||
await saveTrash();
|
await saveTrash();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -219,7 +219,7 @@ function createTrashItemEl(entry) {
|
|||||||
btnRestore.className = 'btn-small';
|
btnRestore.className = 'btn-small';
|
||||||
btnRestore.textContent = t('trash.restore');
|
btnRestore.textContent = t('trash.restore');
|
||||||
btnRestore.title = t('trash.restore_title');
|
btnRestore.title = t('trash.restore_title');
|
||||||
btnRestore.addEventListener('click', () => restoreTrashEntry(entry));
|
btnRestore.addEventListener('click', () => { btnRestore.disabled = true; restoreTrashEntry(entry); });
|
||||||
|
|
||||||
const btnForever = document.createElement('button');
|
const btnForever = document.createElement('button');
|
||||||
btnForever.className = 'btn-danger';
|
btnForever.className = 'btn-danger';
|
||||||
@@ -239,6 +239,10 @@ function createTrashItemEl(entry) {
|
|||||||
* @param {Object} entry
|
* @param {Object} entry
|
||||||
*/
|
*/
|
||||||
async function restoreTrashEntry(entry) {
|
async function restoreTrashEntry(entry) {
|
||||||
|
// Re-Entry-Guard: ein zweiter Klick (z.B. waehrend der Inbox-Alert offen ist) wuerde sonst
|
||||||
|
// das Item ein zweites Mal einfuegen (Duplikat). Nach der ersten Ausfuehrung ist entry
|
||||||
|
// nicht mehr in trash[]; btnRestore wird zusaetzlich beim ersten Klick disabled.
|
||||||
|
if (!trash.includes(entry)) return;
|
||||||
if (entry.type === 'board') {
|
if (entry.type === 'board') {
|
||||||
// Ganzes Board zurueck (inkl. blurred). Falls die id schon existiert (Edge-Case),
|
// Ganzes Board zurueck (inkl. blurred). Falls die id schon existiert (Edge-Case),
|
||||||
// neue uid vergeben, damit nichts ueberschrieben wird.
|
// neue uid vergeben, damit nichts ueberschrieben wird.
|
||||||
|
|||||||
+4
-2
@@ -75,17 +75,19 @@ async function saveTrash() {
|
|||||||
* @param {{ item: Object, type: 'bookmark'|'board', originBoardId: (string|null) }} entry
|
* @param {{ item: Object, type: 'bookmark'|'board', originBoardId: (string|null) }} entry
|
||||||
*/
|
*/
|
||||||
function pushToTrash({ item, type, originBoardId }) {
|
function pushToTrash({ item, type, originBoardId }) {
|
||||||
trash.push({
|
const entry = {
|
||||||
item: structuredClone(item),
|
item: structuredClone(item),
|
||||||
type,
|
type,
|
||||||
originBoardId: originBoardId ?? null,
|
originBoardId: originBoardId ?? null,
|
||||||
deletedAt: Date.now()
|
deletedAt: Date.now()
|
||||||
});
|
};
|
||||||
|
trash.push(entry);
|
||||||
// Aelteste zuerst kappen, falls die Obergrenze ueberschritten ist.
|
// Aelteste zuerst kappen, falls die Obergrenze ueberschritten ist.
|
||||||
if (trash.length > TRASH_MAX_ENTRIES) {
|
if (trash.length > TRASH_MAX_ENTRIES) {
|
||||||
trash.sort((a, b) => a.deletedAt - b.deletedAt);
|
trash.sort((a, b) => a.deletedAt - b.deletedAt);
|
||||||
trash = trash.slice(trash.length - TRASH_MAX_ENTRIES);
|
trash = trash.slice(trash.length - TRASH_MAX_ENTRIES);
|
||||||
}
|
}
|
||||||
|
return entry; // fuer Rollback im Delete-Handler bei Save-Fehler (W-b/Quota)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page-seitiger Wrapper um das DOM-freie ensureInbox() aus quicksave-core.js.
|
// Page-seitiger Wrapper um das DOM-freie ensureInbox() aus quicksave-core.js.
|
||||||
|
|||||||
Reference in New Issue
Block a user