feat(matching): match results as a scoped templ partial + enriched matches
- BookMatch now carries title/author/cover (the fields the old client renderer always assumed but the API never sent — cards would have shown 'undefined'); filled during the family-lens pass in QueryBooks - new partial route GET /unlinked-books/matches renders the scoped match cascade server-side; searchMatches swaps it via htmx.ajax instead of rebuilding cards from JSON - NOTE: the unlinked-books surface is currently dark end-to-end — the templ page has no route, the live linking.ts renderer targets a container no template has, and its Link button calls a window.showMatchModal that does not exist. This commit ships the scoped backend + partial; wiring the UI is a product decision.
This commit is contained in:
1 parent
a31da89df5
commit
327fc2c0f9
5 files changed
+383
-160
No files matched your search
+15
-44
@@ -18,52 +18,23 @@ function searchMatches(
|
||||
matchesList.innerHTML =
|
||||
'<p class="text-sm" style="color: var(--text-secondary)">Searching...</p>';
|
||||
|
||||
const token = getToken();
|
||||
const url = sha256
|
||||
? `/api/books/match?sha256=${sha256}`
|
||||
: `/api/books/match?title=${encodeURIComponent(title)}`;
|
||||
// Rendering ownership: results are server-rendered markup (scoped to
|
||||
// the caller's visible libraries); swap in the templ partial instead of
|
||||
// rebuilding cards from JSON.
|
||||
const params = new URLSearchParams({ progress_id: progressId });
|
||||
if (sha256) params.set("sha256", sha256);
|
||||
if (title) params.set("title", title);
|
||||
|
||||
fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((result) => {
|
||||
if (result.matches && result.matches.length > 0) {
|
||||
matchesList.innerHTML = result.matches
|
||||
.map(
|
||||
(match: any) => `
|
||||
<div class="card p-4 rounded-lg border cursor-pointer hover:shadow-lg transition-shadow"
|
||||
style="background-color: var(--bg-primary); border-color: var(--border);"
|
||||
data-action="auto-link" data-progress-id="${progressId}" data-media-item-id="${match.media_item_id}" data-confidence="${match.confidence}">
|
||||
<div class="flex gap-4">
|
||||
<img src="${match.cover_image_path || "/static/placeholder-book.svg"}"
|
||||
alt="Cover" class="w-16 h-24 object-cover rounded">
|
||||
<div>
|
||||
<h5 class="font-semibold" style="color: var(--text-primary)">${match.title}</h5>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">by ${match.author || "Unknown"}</p>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<span class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: var(--bg-primary);">
|
||||
${Math.round(match.confidence * 100)}% confidence
|
||||
</span>
|
||||
<span class="text-xs" style="color: var(--text-secondary)">${match.match_method}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
} else {
|
||||
matchesList.innerHTML =
|
||||
'<p class="text-sm" style="color: var(--text-secondary)">No matches found. Try manual linking.</p>';
|
||||
}
|
||||
window.htmx
|
||||
.ajax("GET", `/unlinked-books/matches?${params.toString()}`, {
|
||||
target: `#matches-list-${progressId}`,
|
||||
swap: "innerHTML",
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to search", error);
|
||||
matchesList.innerHTML =
|
||||
'<p class="text-sm" style="color: var(--error)">Failed to search</p>';
|
||||
.then((ok) => {
|
||||
if (!ok) {
|
||||
matchesList.innerHTML =
|
||||
'<p class="text-sm" style="color: var(--status-danger)">Failed to search</p>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user