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

@@ -523,10 +523,11 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
const deleteCharacter = useCallback(async function (characterId: string): Promise<void> {
try {
let response: boolean;
const requestData = {characterId: characterId};
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) {
// Series mode - dual logic
const requestData = {characterId, deletedAt};
if (isCurrentlyOffline() || localSeries) {
response = await window.electron.invoke<boolean>('db:series:character:delete', requestData);
} else {
@@ -537,6 +538,7 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
}
} else {
// Pattern A: mutations
const requestData = {characterId, bookId: entityId, deletedAt};
if (isCurrentlyOffline() || book?.localBook) {
// Offline OU livre local → IPC
response = await window.electron.invoke<boolean>('db:character:delete', requestData);
@@ -649,11 +651,12 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
setSelectedCharacter({...selectedCharacter, [section]: updatedSection});
} else {
try {
const requestData = {attributeId: attrId};
const deletedAt: number = System.timeStampInSeconds();
let response: boolean;
if (isSeriesMode) {
// Series mode - dual logic
const requestData = {attributeId: attrId, deletedAt};
if (isCurrentlyOffline() || localSeries) {
response = await window.electron.invoke<boolean>('db:series:character:attribute:delete', requestData);
} else {
@@ -664,6 +667,7 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
}
} else {
// Pattern A: mutations
const requestData = {attributeId: attrId, bookId: entityId, deletedAt};
if (isCurrentlyOffline() || book?.localBook) {
// Offline OU livre local → IPC
response = await window.electron.invoke<boolean>('db:character:attribute:delete', requestData);

View File

@@ -514,9 +514,10 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
const removeSection = useCallback(async function (sectionId: string): Promise<void> {
try {
let success: boolean;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) {
// Series mode - dual logic
const deleteData = {locationId: sectionId};
const deleteData = {locationId: sectionId, deletedAt};
if (isCurrentlyOffline() || localSeries) {
success = await window.electron.invoke<boolean>('db:series:location:delete', deleteData);
} else {
@@ -527,7 +528,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
}
} else {
const requestData = {
locationId: sectionId,
locationId: sectionId, bookId: entityId, deletedAt,
};
if (isCurrentlyOffline() || book?.localBook) {
success = await window.electron.invoke<boolean>('db:location:delete', requestData);
@@ -563,9 +564,10 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
return section.id === sectionId;
})?.elements[elementIndex].id;
let success: boolean;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) {
// Series mode - dual logic
const deleteData = {elementId: elementId};
const deleteData = {elementId, deletedAt};
if (isCurrentlyOffline() || localSeries) {
success = await window.electron.invoke<boolean>('db:series:location:element:delete', deleteData);
} else {
@@ -576,7 +578,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
}
} else {
const requestData = {
elementId: elementId,
elementId, bookId: entityId, deletedAt,
};
if (isCurrentlyOffline() || book?.localBook) {
success = await window.electron.invoke<boolean>('db:location:element:delete', requestData);
@@ -615,9 +617,10 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
return section.id === sectionId;
})?.elements[elementIndex].subElements[subElementIndex].id;
let success: boolean;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) {
// Series mode - dual logic
const deleteData = {subElementId: subElementId};
const deleteData = {subElementId, deletedAt};
if (isCurrentlyOffline() || localSeries) {
success = await window.electron.invoke<boolean>('db:series:location:subelement:delete', deleteData);
} else {
@@ -628,7 +631,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
}
} else {
const requestData = {
subElementId: subElementId,
subElementId, bookId: entityId, deletedAt,
};
if (isCurrentlyOffline() || book?.localBook) {
success = await window.electron.invoke<boolean>('db:location:subelement:delete', requestData);

View File

@@ -296,12 +296,20 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
});
if (response.seriesSpellId) {
const seriesSpellResponse: SeriesSpellDetailResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
'series/spell/detail',
userToken,
lang,
{spellid: response.seriesSpellId}
);
let seriesSpellResponse: SeriesSpellDetailResponse;
if (isCurrentlyOffline() || book?.localBook) {
seriesSpellResponse = await window.electron.invoke<SeriesSpellDetailResponse>(
'db:series:spell:detail',
{spellId: response.seriesSpellId}
);
} else {
seriesSpellResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
'series/spell/detail',
userToken,
lang,
{spellid: response.seriesSpellId}
);
}
if (seriesSpellResponse) {
setSelectedSeriesSpell(seriesSpellResponse);
}
@@ -536,9 +544,10 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
const deleteSpell = useCallback(async function (spellId: string): Promise<void> {
try {
let success: boolean;
const requestData = {spellId};
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) {
// Series mode - dual logic
const requestData = {spellId, deletedAt};
if (isCurrentlyOffline() || localSeries) {
success = await window.electron.invoke<boolean>('db:series:spell:delete', requestData);
} else {
@@ -548,6 +557,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
}
}
} else {
const requestData = {spellId, bookId: entityId, deletedAt};
if (isCurrentlyOffline() || book?.localBook) {
success = await window.electron.invoke<boolean>('db:spell:delete', requestData);
} else {
@@ -844,9 +854,10 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
const deleteTag = useCallback(async function (tagId: string): Promise<boolean> {
try {
let success: boolean;
const deletedAt: number = System.timeStampInSeconds();
if (isSeriesMode) {
// Series mode - dual logic
const deleteData = {tagId};
const deleteData = {tagId, deletedAt};
if (isCurrentlyOffline() || localSeries) {
success = await window.electron.invoke<boolean>('db:series:spell:tag:delete', deleteData);
} else {
@@ -856,7 +867,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
}
}
} else {
const requestData = {tagId, bookId: entityId};
const requestData = {tagId, bookId: entityId, deletedAt};
if (isCurrentlyOffline() || book?.localBook) {
success = await window.electron.invoke<boolean>('db:spell:tag:delete', requestData);
} else {
@@ -885,17 +896,25 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
const handleSyncComplete = useCallback(async function (): Promise<void> {
if (selectedSpell?.seriesSpellId) {
const seriesSpellResponse: SeriesSpellDetailResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
'series/spell/detail',
userToken,
lang,
{spellid: selectedSpell.seriesSpellId}
);
let seriesSpellResponse: SeriesSpellDetailResponse;
if (isCurrentlyOffline() || (isSeriesMode ? localSeries : book?.localBook)) {
seriesSpellResponse = await window.electron.invoke<SeriesSpellDetailResponse>(
'db:series:spell:detail',
{spellId: selectedSpell.seriesSpellId}
);
} else {
seriesSpellResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
'series/spell/detail',
userToken,
lang,
{spellid: selectedSpell.seriesSpellId}
);
}
if (seriesSpellResponse) {
setSelectedSeriesSpell(seriesSpellResponse);
}
}
}, [selectedSpell?.seriesSpellId, userToken, lang]);
}, [selectedSpell?.seriesSpellId, userToken, lang, isCurrentlyOffline, isSeriesMode, localSeries, book?.localBook]);
const enterDetailMode = useCallback(async function (spell: SpellListItem): Promise<void> {
await selectSpell(spell);

View File

@@ -186,6 +186,12 @@ export default function useSyncBooks() {
}
}
async function syncAllFromServer(): Promise<void> {
for (const diff of booksToSyncFromServer) {
await syncFromServer(diff.id);
}
}
async function refreshBooks(): Promise<void> {
try {
let localBooksResponse: SyncedBook[] = [];
@@ -250,6 +256,7 @@ export default function useSyncBooks() {
syncFromServer,
syncToServer,
syncAllToServer,
syncAllFromServer,
refreshBooks,
localOnlyBooks,
serverOnlyBooks,

View File

@@ -275,10 +275,12 @@ export default function useSyncSeries() {
}
}
/**
* Refreshes the sync status of all series by comparing local and server data.
* Updates the context with the latest sync information.
*/
async function syncAllFromServer(): Promise<void> {
for (const diff of seriesToSyncFromServer) {
await syncFromServer(diff.id);
}
}
async function refreshSeries(): Promise<void> {
try {
let localSeriesResponse: SyncedSeries[] = [];
@@ -343,6 +345,7 @@ export default function useSyncSeries() {
syncFromServer,
syncToServer,
syncAllToServer,
syncAllFromServer,
refreshSeries,
localOnlySeries,
serverOnlySeries,