Add deletedAt timestamps to delete operations for better audit tracking

- Updated delete methods across hooks and components to include `deletedAt: System.timeStampInSeconds()`.
- Refactored synchronized delete logic to pass `deletedAt` for both offline and online states.
- Improved synchronization workflows to include `deletedAt` in server and IPC requests.
- Enhanced destructuring patterns for cleaner and more consistent request data.
This commit is contained in:
natreex
2026-02-09 17:12:03 -05:00
parent 209dc6f85a
commit 49bb6e06f5
14 changed files with 146 additions and 92 deletions

View File

@@ -1,7 +1,7 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCloud, faCloudArrowDown, faCloudArrowUp, faSpinner} from "@fortawesome/free-solid-svg-icons";
import {useTranslations} from "next-intl";
import {useState, useContext} from "react";
import {useState, useEffect, useContext} from "react";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {SyncType} from "@/context/BooksSyncContext";
import useSyncBooks from "@/hooks/useSyncBooks";
@@ -18,6 +18,10 @@ export default function SyncBook({bookId, status}: SyncBookProps) {
const [currentStatus, setCurrentStatus] = useState<SyncType>(status);
const {upload: hookUpload, download: hookDownload, syncFromServer: hookSyncFromServer, syncToServer: hookSyncToServer} = useSyncBooks();
useEffect((): void => {
setCurrentStatus(status);
}, [status]);
const isOffline: boolean = isCurrentlyOffline();
async function upload(): Promise<void> {

View File

@@ -486,8 +486,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
let response: boolean;
const elementId = sections.find((section: LocationProps): boolean => section.id === sectionId)
?.elements[elementIndex].id;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) {
const deleteData = {elementId: elementId};
const deleteData = {elementId: elementId, deletedAt};
if (isCurrentlyOffline() || localSeries) {
response = await window.electron.invoke<boolean>('db:series:location:element:delete', deleteData);
} else {
@@ -498,16 +499,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
}
} else if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:location:element:delete', {
elementId: elementId,
elementId: elementId, bookId: currentEntityId, deletedAt,
});
} else {
response = await System.authDeleteToServer<boolean>(`location/element/delete`, {
elementId: elementId,
elementId: elementId, bookId: currentEntityId, deletedAt,
}, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
addToQueue('db:location:element:delete', {
elementId: elementId,
elementId: elementId, bookId: currentEntityId, deletedAt,
});
}
}
@@ -536,8 +537,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
try {
let response: boolean;
const subElementId = sections.find((section: LocationProps): boolean => section.id === sectionId)?.elements[elementIndex].subElements[subElementIndex].id;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) {
const deleteData = {subElementId: subElementId};
const deleteData = {subElementId: subElementId, deletedAt};
if (isCurrentlyOffline() || localSeries) {
response = await window.electron.invoke<boolean>('db:series:location:subelement:delete', deleteData);
} else {
@@ -548,16 +550,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
}
} else if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:location:subelement:delete', {
subElementId: subElementId,
subElementId: subElementId, bookId: currentEntityId, deletedAt,
});
} else {
response = await System.authDeleteToServer<boolean>(`location/sub-element/delete`, {
subElementId: subElementId,
subElementId: subElementId, bookId: currentEntityId, deletedAt,
}, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
addToQueue('db:location:subelement:delete', {
subElementId: subElementId,
subElementId: subElementId, bookId: currentEntityId, deletedAt,
});
}
}
@@ -581,8 +583,9 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
async function handleRemoveSection(sectionId: string): Promise<void> {
try {
let response: boolean;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) {
const deleteData = {locationId: sectionId};
const deleteData = {locationId: sectionId, deletedAt};
if (isCurrentlyOffline() || localSeries) {
response = await window.electron.invoke<boolean>('db:series:location:delete', deleteData);
} else {
@@ -593,16 +596,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
}
} else if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:location:delete', {
locationId: sectionId,
locationId: sectionId, bookId: currentEntityId, deletedAt,
});
} else {
response = await System.authDeleteToServer<boolean>(`location/delete`, {
locationId: sectionId,
locationId: sectionId, bookId: currentEntityId, deletedAt,
}, token, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
addToQueue('db:location:delete', {
locationId: sectionId,
locationId: sectionId, bookId: currentEntityId, deletedAt,
});
}
}

View File

@@ -132,7 +132,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
async function deleteIncident(actId: number, incidentId: string): Promise<void> {
try {
let response: boolean;
const deleteData = { bookId, incidentId };
const deleteData = { bookId, incidentId, deletedAt: System.timeStampInSeconds() };
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:incident:remove', deleteData);
} else {
@@ -223,7 +223,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
async function deletePlotPoint(actId: number, plotPointId: string): Promise<void> {
try {
let response: boolean;
const deleteData = { plotId: plotPointId };
const deleteData = { plotId: plotPointId, bookId, deletedAt: System.timeStampInSeconds() };
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:plot:remove', deleteData);
} else {
@@ -365,7 +365,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
): Promise<void> {
try {
let response: boolean;
const unlinkData = { chapterInfoId };
const unlinkData = { chapterInfoId, bookId, deletedAt: System.timeStampInSeconds() };
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:chapter:information:remove', unlinkData);
} else {

View File

@@ -88,10 +88,12 @@ export default function Issues({issues, setIssues}: IssuesProps) {
try {
let response: boolean;
const deletedAt: number = System.timeStampInSeconds();
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:issue:remove', {
bookId,
issueId,
deletedAt,
});
} else {
response = await System.authDeleteToServer<boolean>(
@@ -99,6 +101,7 @@ export default function Issues({issues, setIssues}: IssuesProps) {
{
bookId,
issueId,
deletedAt,
},
token,
lang
@@ -108,6 +111,7 @@ export default function Issues({issues, setIssues}: IssuesProps) {
addToQueue('db:book:issue:remove', {
bookId,
issueId,
deletedAt,
});
}
}

View File

@@ -65,8 +65,9 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
try {
let response: boolean;
const elementId = (worlds[selectedWorldIndex][section] as WorldElement[])[index].id;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) {
const deleteData = {elementId: elementId};
const deleteData = {elementId, deletedAt};
if (isCurrentlyOffline() || localSeries) {
response = await window.electron.invoke<boolean>('db:series:world:element:delete', deleteData);
} else {
@@ -77,16 +78,16 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
}
} else if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:world:element:remove', {
elementId: elementId,
elementId, bookId: book?.bookId, deletedAt,
});
} else {
response = await System.authDeleteToServer<boolean>('book/world/element/delete', {
elementId: elementId,
elementId, bookId: book?.bookId, deletedAt,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:book:world:element:remove', {
elementId: elementId,
elementId: elementId, bookId: book?.bookId, deletedAt,
});
}
}

View File

@@ -188,16 +188,25 @@ export default function ScribeChapterComponent() {
try {
setDeleteConfirmationMessage(false);
let response:boolean = false;
const deletedAt: number = System.timeStampInSeconds();
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:chapter:remove', removeChapterId);
response = await window.electron.invoke<boolean>('db:chapter:remove', {
chapterId: removeChapterId,
bookId: book?.bookId,
deletedAt,
});
} else {
response = await System.authDeleteToServer<boolean>('chapter/remove', {
chapterId: removeChapterId,
bookId: book?.bookId,
deletedAt,
}, userToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:chapter:remove', {
chapterId: removeChapterId,
bookId: book?.bookId,
deletedAt,
});
}
}

View File

@@ -58,7 +58,7 @@ export default function SeriesSettingSidebar(
async function handleDeleteSeries(): Promise<void> {
try {
const deleteData = {seriesId: seriesId};
const deleteData = {seriesId: seriesId, deletedAt: System.timeStampInSeconds()};
let success: boolean;
if (isCurrentlyOffline() || localSeries) {

View File

@@ -174,7 +174,8 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
try {
const removeData = {
seriesId: seriesId,
bookId: bookId
bookId: bookId,
deletedAt: System.timeStampInSeconds(),
};
let response: boolean;