fix(web): banner highlight delete; retention wording states the days

The book-detail banner's highlight rows gain Delete (tombstoned
server-side, restorable from the Deleted modal — same as the reader
drawers already offered). The deleted-annotations modal now says the
retention in plain language — 'kept for 30 days' from the
annotation_tombstone_ttl_days tunable (threaded through BookDetail →
NotesHighlightsModal; 1-day singular and retention-disabled variants
handled) — instead of referencing a settings name a reader cannot see.
This commit is contained in:
John O'Keefe committed 2026-10-05 12:15:45 -04:00
1 parent 5275f2ba38
commit f45adc6362
7 files changed
+502 -444

No files matched your search

+4 -1
View File
@@ -1436,7 +1436,10 @@ func registerFrontendRoutes(cfg *Config) {
// Render template // Render template
var buf bytes.Buffer var buf bytes.Buffer
err = templates.BookDetail(user, detail, "").Render(c.Request().Context(), &buf) // The deleted-annotations modal states the retention in plain days
// (the tunable's user-facing value), never as a settings name.
tombstoneDays := cfg.Settings.TombstoneTTL() / (24 * time.Hour)
err = templates.BookDetail(user, detail, "", int(tombstoneDays)).Render(c.Request().Context(), &buf)
if err != nil { if err != nil {
return err return err
} }
+1 -2
View File
@@ -99,8 +99,7 @@ templ AnnotationsBanner(book handlers.MediaDetail) {
></button> ></button>
<button <button
type="button" type="button"
x-show="row.kind === 'note'" @click="row.kind === 'note' ? deleteBannerNote(row) : deleteBannerHighlight(row)"
@click="deleteBannerNote(row)"
class="text-xs px-2 py-1 rounded-lg hover:bg-surface-hover" class="text-xs px-2 py-1 rounded-lg hover:bg-surface-hover"
style="color: var(--status-danger);" style="color: var(--status-danger);"
>✕ Delete</button> >✕ Delete</button>
+2 -2
View File
@@ -9,7 +9,7 @@ import (
"strings" "strings"
) )
templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) { templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tombstoneDays int) {
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -440,7 +440,7 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
} }
</div> </div>
@ProgressSyncModal(user, book) @ProgressSyncModal(user, book)
@NotesHighlightsModal(user, book) @NotesHighlightsModal(user, book, tombstoneDays)
@AnnotationsBanner(book) @AnnotationsBanner(book)
if user.Role == "admin" { if user.Role == "admin" {
@MetadataEditorModal(book) @MetadataEditorModal(book)
+8 -2
View File
@@ -128,7 +128,7 @@ templ ProgressSyncModal(user User, book handlers.MediaDetail) {
</div> </div>
} }
templ NotesHighlightsModal(user User, book handlers.MediaDetail) { templ NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays int) {
<div <div
id="notes-modal" id="notes-modal"
class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4"
@@ -183,7 +183,13 @@ templ NotesHighlightsModal(user User, book handlers.MediaDetail) {
<span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">{ len(book.DeletedAnnotations) }</span> <span class="badge" style="background-color: var(--accent-muted); color: var(--accent);">{ len(book.DeletedAnnotations) }</span>
</div> </div>
<p class="text-xs mb-4" style="color: var(--text-secondary);"> <p class="text-xs mb-4" style="color: var(--text-secondary);">
Deleted entries are kept for the sync retention window before being removed automatically. Restore returns them to every synced device; Delete permanently removes them immediately. if tombstoneDays == 1 {
Deleted entries are kept for 1 day before being removed automatically. Restore returns them to every synced device; Delete permanently removes them immediately.
} else if tombstoneDays > 1 {
Deleted entries are kept for { tombstoneDays } days before being removed automatically. Restore returns them to every synced device; Delete permanently removes them immediately.
} else {
Deleted entries are kept until manually purged (retention disabled). Restore returns them to every synced device; Delete permanently removes them immediately.
}
</p> </p>
<div class="space-y-3"> <div class="space-y-3">
for _, ann := range book.DeletedAnnotations { for _, ann := range book.DeletedAnnotations {
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -17,7 +17,7 @@ import (
"strings" "strings"
) )
func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ.Component { func BookDetail(user User, book handlers.MediaDetail, errorMessage string, tombstoneDays int) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -1396,7 +1396,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = NotesHighlightsModal(user, book).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = NotesHighlightsModal(user, book, tombstoneDays).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+17
View File
@@ -311,6 +311,23 @@ Alpine.data("bookDetail", () => {
showToast("Could not delete the note", "error"); showToast("Could not delete the note", "error");
} }
}, },
async deleteBannerHighlight(row: AnnotationRow) {
if (row.kind !== "highlight") return;
const mediaId = getMediaId();
const hlId = row.key.slice("hl-".length);
try {
const resp = await fetch(`/api/media-items/${mediaId}/highlights/${hlId}`, {
method: "DELETE",
headers: { Authorization: getAuthHeader() },
});
if (!resp.ok) throw new Error(String(resp.status));
// Tombstoned server-side: restorable from the Deleted modal.
showToast("Highlight deleted — restorable from Deleted", "success");
await this.fetchBannerAnnotations();
} catch (_e) {
showToast("Could not delete the highlight", "error");
}
},
showExportDialog(): void { showExportDialog(): void {
if (this.bannerRows.length === 0) return; if (this.bannerRows.length === 0) return;
this.annotationsExportOpen = true; this.annotationsExportOpen = true;