From f45adc636260fa62395784d2cd8026e98457e42c Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Mon, 5 Oct 2026 12:15:45 -0400 Subject: [PATCH] fix(web): banner highlight delete; retention wording states the days MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/router/frontend.go | 5 +- templates/annotations_banner.templ | 3 +- templates/book_detail.templ | 4 +- templates/book_detail_modals.templ | 10 +- templates/book_detail_modals_templ.go | 903 +++++++++++++------------- templates/book_detail_templ.go | 4 +- web/src/book-detail.ts | 17 + 7 files changed, 502 insertions(+), 444 deletions(-) diff --git a/internal/router/frontend.go b/internal/router/frontend.go index 46eae09a..e7894b16 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -1436,7 +1436,10 @@ func registerFrontendRoutes(cfg *Config) { // Render template 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 { return err } diff --git a/templates/annotations_banner.templ b/templates/annotations_banner.templ index cedbc94d..e71d9b8a 100644 --- a/templates/annotations_banner.templ +++ b/templates/annotations_banner.templ @@ -99,8 +99,7 @@ templ AnnotationsBanner(book handlers.MediaDetail) { > diff --git a/templates/book_detail.templ b/templates/book_detail.templ index 20f9302e..a8a9abb6 100644 --- a/templates/book_detail.templ +++ b/templates/book_detail.templ @@ -9,7 +9,7 @@ import ( "strings" ) -templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) { +templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tombstoneDays int) { @@ -440,7 +440,7 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) { } @ProgressSyncModal(user, book) - @NotesHighlightsModal(user, book) + @NotesHighlightsModal(user, book, tombstoneDays) @AnnotationsBanner(book) if user.Role == "admin" { @MetadataEditorModal(book) diff --git a/templates/book_detail_modals.templ b/templates/book_detail_modals.templ index 8c3add39..78070c8b 100644 --- a/templates/book_detail_modals.templ +++ b/templates/book_detail_modals.templ @@ -128,7 +128,7 @@ templ ProgressSyncModal(user User, book handlers.MediaDetail) { } -templ NotesHighlightsModal(user User, book handlers.MediaDetail) { +templ NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays int) {

- 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. + }

for _, ann := range book.DeletedAnnotations { diff --git a/templates/book_detail_modals_templ.go b/templates/book_detail_modals_templ.go index 80d22260..6e521957 100644 --- a/templates/book_detail_modals_templ.go +++ b/templates/book_detail_modals_templ.go @@ -335,7 +335,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component { }) } -func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component { +func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays int) templ.Component { 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 if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -447,64 +447,97 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

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.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - for _, ann := range book.DeletedAnnotations { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "

1 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "Deleted entries are kept for ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(ann.ID) + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(tombstoneDays) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 190, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 189, Col: 51} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " days before being removed automatically. Restore returns them to every synced device; Delete permanently removes them immediately.") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "Deleted entries are kept until manually purged (retention disabled). Restore returns them to every synced device; Delete permanently removes them immediately.") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, ann := range book.DeletedAnnotations { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(ann.DeletedAt, user.Timezone)) + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(ann.AnnotationType) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 195, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 199, Col: 119} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "Delete permanently
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if ann.DisplayText != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(ann.DisplayText) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 217, Col: 94} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

(no text)

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if ann.SecondaryText != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(ann.SecondaryText) + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(ann.DisplayText) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 222, Col: 103} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 223, Col: 94} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "

(no text)

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "
") + if ann.SecondaryText != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var30 string + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(ann.SecondaryText) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 228, Col: 103} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -614,12 +647,12 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var30 := templ.GetChildren(ctx) - if templ_7745c5c3_Var30 == nil { - templ_7745c5c3_Var30 = templ.NopComponent + templ_7745c5c3_Var31 := templ.GetChildren(ctx) + if templ_7745c5c3_Var31 == nil { + templ_7745c5c3_Var31 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -627,7 +660,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "

Edit Metadata

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if book.CoverImagePath.Valid && book.CoverImagePath.String != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\"")") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\"")") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" class=\"w-full h-full object-cover\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\"")") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "
Click to upload
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, tag := range book.Tags { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "\" class=\"hidden\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "
Unknown
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "Save
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/book_detail_templ.go b/templates/book_detail_templ.go index 7eb3431d..fda4b32c 100644 --- a/templates/book_detail_templ.go +++ b/templates/book_detail_templ.go @@ -17,7 +17,7 @@ import ( "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) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 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 { 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 { return templ_7745c5c3_Err } diff --git a/web/src/book-detail.ts b/web/src/book-detail.ts index 009318e1..3dce2aeb 100644 --- a/web/src/book-detail.ts +++ b/web/src/book-detail.ts @@ -311,6 +311,23 @@ Alpine.data("bookDetail", () => { 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 { if (this.bannerRows.length === 0) return; this.annotationsExportOpen = true;