chore(web): delete dead linking module
The module could never execute against any real page: - renderUnlinkedBooks targeted #unlinked-books-list, an id that exists in no template; the live /unlinked-books page is server-rendered by templates.UnlinkedBooks and driven by the Alpine unlinkedBooks component (web/src/unlinked_books.ts, setupEventDelegation) with its own CSP-safe wiring. - showSuggestionsModal/hideMatchModal need #match-modal and #match-modal-content, which no template renders either. - Its inline onclick handlers referenced window.showMatchModal, a function that was never defined anywhere. Nothing imports it except main.ts's side-effect import, and no template/Go/TS surface references its Alpine component registration, containers, or globals (grepped templates/, internal/, web/src/). Verified after removal: tsc, vp check, vitest (198), and /unlinked-books plus dashboard/stats load with zero console errors, empty state and Alpine component intact.
This commit is contained in:
1 parent
ef4de7896a
commit
788e9463cf
2 files changed
-214
No files matched your search
@@ -1,213 +0,0 @@
|
||||
import { Alpine } from "./alpine";
|
||||
import { authHeaders } from "./storage";
|
||||
import { showToast } from "./toast";
|
||||
|
||||
async function loadUnlinkedBooks(): Promise<void> {
|
||||
// cookie auth carries the call; a null bearer must not strand it
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/sync/unlinked-books", {
|
||||
headers: authHeaders(),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
renderUnlinkedBooks(data.unlinked || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load unlinked books:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderUnlinkedBooks(books: UnlinkedBookData[]): void {
|
||||
const container = document.getElementById("unlinked-books-list");
|
||||
if (!container) return;
|
||||
|
||||
if (books.length === 0) {
|
||||
container.innerHTML =
|
||||
'<p class="text-center p-4" style="color: var(--text-secondary)">No unlinked books</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = books
|
||||
.map(
|
||||
(book) => `
|
||||
<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)">${book.title_from_device}</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">${book.device_name} (${book.device_type})</p>
|
||||
<p class="text-xs" style="color: var(--text-secondary)">${book.file_path}</p>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Confidence: ${Math.round(book.confidence_score * 100)}%</p>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button data-show-match="${book.progress_id}" class="btn-primary px-3 py-1 rounded text-sm">Link</button>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
book.potential_matches && book.potential_matches.length > 0
|
||||
? `
|
||||
<div class="mt-3 pt-3 border-t" style="border-color: var(--border)">
|
||||
<p class="text-xs font-medium mb-2" style="color: var(--text-secondary)">Potential Matches:</p>
|
||||
${book.potential_matches
|
||||
.slice(0, 3)
|
||||
.map(
|
||||
(match) => `
|
||||
<div class="flex justify-between items-center p-2 rounded mb-1" style="background-color: var(--bg-primary)">
|
||||
<div>
|
||||
<p class="text-sm" style="color: var(--text-primary)">${match.title}</p>
|
||||
<p class="text-xs" style="color: var(--text-secondary)">${match.author} (${Math.round(match.confidence * 100)}%)</p>
|
||||
</div>
|
||||
<button data-link-book="${book.progress_id}" data-media-id="${match.media_item_id}" class="btn-secondary px-2 py-1 rounded text-xs">Link</button>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function linkBook(progressId: string, mediaItemId: string): Promise<void> {
|
||||
// cookie auth carries the call; a null bearer must not strand it
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/sync/link-book", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
progress_id: progressId,
|
||||
media_item_id: mediaItemId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast("Book linked successfully", "success");
|
||||
loadUnlinkedBooks();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast(error.error || "Failed to link book", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to link book:", error);
|
||||
showToast("Failed to link book", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function autoLinkBooks(): Promise<void> {
|
||||
// cookie auth carries the call; a null bearer must not strand it
|
||||
|
||||
if (!confirm("Auto-link all books with high confidence matches?")) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/sync/auto-link", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ confidence_threshold: 0.9 }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
showToast(`Auto-linked ${data.linked_count || 0} books`, "success");
|
||||
loadUnlinkedBooks();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to auto-link:", error);
|
||||
showToast("Failed to auto-link books", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function getSuggestions(progressId: string): Promise<void> {
|
||||
// cookie auth carries the call; a null bearer must not strand it
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sync/suggestions/${progressId}`, {
|
||||
headers: authHeaders(),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const suggestions = await response.json();
|
||||
showSuggestionsModal(progressId, suggestions);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get suggestions:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function showSuggestionsModal(progressId: string, suggestions: PotentialMatchData[]): void {
|
||||
const modal = document.getElementById("match-modal");
|
||||
const content = document.getElementById("match-modal-content");
|
||||
|
||||
if (!modal || !content) return;
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="p-4">
|
||||
<h3 class="font-medium mb-4" style="color: var(--text-primary)">Select a match</h3>
|
||||
<div class="space-y-2">
|
||||
${suggestions
|
||||
.map(
|
||||
(s) => `
|
||||
<div class="p-3 rounded border cursor-pointer hover:border-opacity-50"
|
||||
style="background-color: var(--bg-primary); border-color: var(--border)"
|
||||
data-link-book="${progressId}" data-media-id="${s.media_item_id}">
|
||||
<p class="font-medium" style="color: var(--text-primary)">${s.title}</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">${s.author}</p>
|
||||
<p class="text-xs" style="color: var(--text-secondary)">${Math.round(s.confidence * 100)}% match</p>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
<button data-hide-match class="mt-4 btn-secondary w-full py-2 rounded">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function hideMatchModal(): void {
|
||||
const modal = document.getElementById("match-modal");
|
||||
if (modal) {
|
||||
modal.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// Delegated click wiring for the generated markup above: the CSP build
|
||||
// refuses inline onclick= handlers (and module functions are not globals).
|
||||
// The row-level "Link" opens the suggestions flow via getSuggestions — the
|
||||
// old window.showMatchModal it referenced never existed.
|
||||
document.addEventListener("click", (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const show = target.closest<HTMLElement>("[data-show-match]");
|
||||
if (show) {
|
||||
void getSuggestions(show.dataset.showMatch ?? "");
|
||||
return;
|
||||
}
|
||||
const link = target.closest<HTMLElement>("[data-link-book]");
|
||||
if (link) {
|
||||
void linkBook(link.dataset.linkBook ?? "", link.dataset.mediaId ?? "");
|
||||
hideMatchModal();
|
||||
return;
|
||||
}
|
||||
if (target.closest("[data-hide-match]")) {
|
||||
hideMatchModal();
|
||||
}
|
||||
});
|
||||
|
||||
Alpine.data("linking", () => ({
|
||||
autoLinkBooks,
|
||||
getSuggestions,
|
||||
hideMatchModal,
|
||||
linkBook,
|
||||
loadUnlinkedBooks,
|
||||
showSuggestionsModal,
|
||||
}));
|
||||
@@ -20,7 +20,6 @@ import "./index";
|
||||
import "./image_fallback";
|
||||
import { armAuthFormRedirects } from "./auth_forms";
|
||||
import "./library-switcher";
|
||||
import "./linking";
|
||||
import "./login";
|
||||
import "./password_validation";
|
||||
import "./processing_issues";
|
||||
|
||||
Reference in new issue
Block a user