feat(ui): inline conflict resolution with Keep This button

Replace the 'Go to Conflicts Page' link with inline conflict resolution.
Each conflict source now has a 'Keep This' button that resolves the
conflict directly from the book detail page.

- Conflict data now keyed by source name (koreader, web) instead of
  new/existing, with Source and Timestamp fields
- Display percentage scaled correctly (* 100)
- Fix page field name from current_page to page
- Add conflict resolution JavaScript in book-detail.ts
- Add 10-minute cooldown after resolution to prevent re-detection
This commit is contained in:
2026-06-02 19:46:17 -04:00
parent f57d64563b
commit bc59159cde
4 changed files with 491 additions and 412 deletions
+28
View File
@@ -104,6 +104,7 @@ interface MetadataEditorState {
generateCover(): Promise<void>;
removeCover(): void;
saveMetadata(): Promise<void>;
resolveConflict(conflictId: string, winner: string): Promise<void>;
}
Alpine.data("bookDetail", () => {
@@ -149,6 +150,33 @@ Alpine.data("bookDetail", () => {
hideProgressSyncModal,
hideNotesModal,
async resolveConflict(conflictId: string, winner: string) {
try {
const resp = await fetch(`/api/conflicts/${conflictId}/resolve`, {
method: "POST",
headers: {
Authorization: getAuthHeader(),
"Content-Type": "application/json",
},
body: JSON.stringify({ winner }),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || err.message || "Failed to resolve conflict");
}
hideProgressSyncModal();
showToast("Conflict resolved successfully", "success");
setTimeout(() => window.location.reload(), 500);
} catch (e) {
showToast(
e instanceof Error ? e.message : "Failed to resolve conflict",
"error",
);
}
},
init() {
const link = document.getElementById("back-link");
if (!link) return;