- Add search.ts - header search with keyboard navigation - Debounced search with 300ms delay - Arrow key navigation through results - Escape to close, Enter to select - Library type icons and highlighting - Add collections.ts - collection and rule management - Rule CRUD operations (create, update, delete) - Rule testing functionality - Bulk collection operations - Add bookshelf.ts - book display and navigation - Library selection state management - Book viewing interactions - Pagination logic - Add linking.ts - book matching and manual linking - Search and match functionality - Manual link modal - Bulk auto-link and suggestions - Add api-explorer.ts - API testing interface - Request/response display - cURL command generation - History tracking - Add admin.ts - admin dashboard actions - Library scan triggers - System statistics display - Profile management - Add analytics.ts - analytics data loading - Chart.js integration - Daily reading minutes chart - Device usage and popular books display - Add queue.ts - sync queue management - Process pending items - Clear failed/all items - Filter by status, type, device - Add conflicts.ts - conflict resolution - Individual and bulk resolve operations - Winner device selection - Manual override inputs - Add docs.ts - documentation search - Lunr.js search integration - Sidebar toggle for mobile
213 lines
7.5 KiB
TypeScript
213 lines
7.5 KiB
TypeScript
import type { ConflictDetailResponse, ConflictListResponse, BulkResolveResponse } from './types/api';
|
|
|
|
async function refreshConflicts(): Promise<void> {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/conflicts', {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data: ConflictListResponse = await response.json();
|
|
renderConflicts(data.conflicts);
|
|
updateConflictStats(data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to refresh conflicts:', error);
|
|
}
|
|
}
|
|
|
|
async function resolveConflict(conflictId: string, winner: string, manualData?: Record<string, unknown>): Promise<void> {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/conflicts/${conflictId}/resolve`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ winner, manual_data: manualData })
|
|
});
|
|
|
|
if (response.ok) {
|
|
if ((window as any).showToast?.success) {
|
|
(window as any).showToast.success('Conflict resolved');
|
|
}
|
|
refreshConflicts();
|
|
} else {
|
|
const error = await response.json();
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error(error.error || 'Failed to resolve conflict');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to resolve conflict:', error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error('Failed to resolve conflict');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function bulkResolve(strategy: 'most_recent' | 'highest_progress', conflictIds: string[]): Promise<void> {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/conflicts/bulk-resolve', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ conflict_ids: conflictIds, strategy })
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data: BulkResolveResponse = await response.json();
|
|
if ((window as any).showToast?.success) {
|
|
(window as any).showToast.success(`Resolved ${data.success} conflicts`);
|
|
}
|
|
refreshConflicts();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to bulk resolve:', error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error('Failed to bulk resolve conflicts');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function bulkDismiss(conflictIds: string[]): Promise<void> {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/conflicts/bulk-dismiss', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ conflict_ids: conflictIds })
|
|
});
|
|
|
|
if (response.ok) {
|
|
if ((window as any).showToast?.success) {
|
|
(window as any).showToast.success('Conflicts dismissed');
|
|
}
|
|
refreshConflicts();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to dismiss conflicts:', error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error('Failed to dismiss conflicts');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function dismissAllResolved(): Promise<void> {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/conflicts/dismiss-resolved', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (response.ok) {
|
|
if ((window as any).showToast?.success) {
|
|
(window as any).showToast.success('Resolved conflicts dismissed');
|
|
}
|
|
refreshConflicts();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to dismiss resolved:', error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error('Failed to dismiss resolved conflicts');
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderConflicts(conflicts: ConflictDetailResponse[]): void {
|
|
const container = document.getElementById('conflicts-list');
|
|
if (!container) return;
|
|
|
|
if (conflicts.length === 0) {
|
|
container.innerHTML = '<p class="text-center p-4" style="color: var(--text-secondary)">No conflicts found</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = conflicts.map(conflict => `
|
|
<div class="p-4 rounded-lg border mb-2" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h3 class="font-medium" style="color: var(--text-primary)">${conflict.media_item_title}</h3>
|
|
<p class="text-sm" style="color: var(--text-secondary)">${conflict.conflict_type} - ${conflict.resolution_status}</p>
|
|
</div>
|
|
${conflict.resolution_status === 'unresolved' ? `
|
|
<div class="flex space-x-2">
|
|
<button onclick="window.showResolveModal('${conflict.id}')" class="btn-primary px-3 py-1 rounded text-sm">Resolve</button>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function updateConflictStats(data: ConflictListResponse): void {
|
|
const totalEl = document.getElementById('conflicts-total');
|
|
const unresolvedEl = document.getElementById('conflicts-unresolved');
|
|
|
|
if (totalEl) totalEl.textContent = String(data.total);
|
|
if (unresolvedEl) unresolvedEl.textContent = String(data.unresolved);
|
|
}
|
|
|
|
function showResolveModal(conflictId: string): void {
|
|
const modal = document.getElementById('resolve-modal');
|
|
const conflictIdInput = document.getElementById('resolve-conflict-id') as HTMLInputElement;
|
|
|
|
if (modal && conflictIdInput) {
|
|
conflictIdInput.value = conflictId;
|
|
modal.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
function hideResolveModal(): void {
|
|
const modal = document.getElementById('resolve-modal');
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function handleResolveSubmit(event: Event): void {
|
|
event.preventDefault();
|
|
|
|
const form = event.target as HTMLFormElement;
|
|
const conflictId = (form.querySelector('#resolve-conflict-id') as HTMLInputElement)?.value;
|
|
const winner = (form.querySelector('input[name="winner"]:checked') as HTMLInputElement)?.value;
|
|
|
|
if (!conflictId || !winner) {
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error('Please select a winner');
|
|
}
|
|
return;
|
|
}
|
|
|
|
resolveConflict(conflictId, winner);
|
|
hideResolveModal();
|
|
}
|
|
|
|
(window as any).refreshConflicts = refreshConflicts;
|
|
(window as any).resolveConflict = resolveConflict;
|
|
(window as any).bulkResolve = bulkResolve;
|
|
(window as any).bulkDismiss = bulkDismiss;
|
|
(window as any).dismissAllResolved = dismissAllResolved;
|
|
(window as any).showResolveModal = showResolveModal;
|
|
(window as any).hideResolveModal = hideResolveModal;
|
|
(window as any).handleResolveSubmit = handleResolveSubmit;
|