feat: add notes field to favorites and enhance OG meta tags

Add an optional long-form "notes" text field to each favorite for
reviews, thoughts, or extended descriptions. The field is stored in
SQLite via a new migration (002_add_fave_notes.sql) and propagated
through the entire stack:

- Model: Notes field on Fave struct
- Store: All SQL queries (Create, GetByID, Update, list methods,
  scanFaves) updated with notes column
- Web handlers: Read/write notes in create, edit, update forms
- API handlers: Notes in create, update, get, import request/response
- Export: Notes included in both JSON and CSV exports
- Import: Notes parsed from both JSON and CSV imports
- Feed: Notes used as Atom feed item summary when present
- Form template: New textarea between URL and image fields
- Detail template: Display notes, enhanced og:description with
  cascade: notes (truncated) → URL → generic fallback text

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-04-04 00:40:08 +02:00
commit 485d01ce45
14 changed files with 151 additions and 71 deletions

View file

@ -222,7 +222,7 @@ func TestAPIGetFave(t *testing.T) {
// Create a public fave directly.
user, _ := users.GetByUsername("testuser")
fave, _ := h.deps.Faves.Create(user.ID, "Test fave", "https://example.com", "", "public")
fave, _ := h.deps.Faves.Create(user.ID, "Test fave", "https://example.com", "", "", "public")
h.deps.Tags.SetFaveTags(fave.ID, []string{"test"})
req := httptest.NewRequest("GET", "/api/v1/faves/"+faveIDStr(fave.ID), nil)
@ -257,7 +257,7 @@ func TestAPIPrivateFaveHiddenFromOthers(t *testing.T) {
// User A creates a private fave.
userA, _ := users.Create("usera", "pass123", "user")
fave, _ := h.deps.Faves.Create(userA.ID, "Secret", "", "", "private")
fave, _ := h.deps.Faves.Create(userA.ID, "Secret", "", "", "", "private")
// User B tries to access it.
cookieB := apiLogin(t, users, sessions, "userb", "pass123", "user")
@ -276,7 +276,7 @@ func TestAPIPrivateFaveVisibleToOwner(t *testing.T) {
h, mux, users, sessions := testAPIServer(t)
userA, _ := users.Create("usera", "pass123", "user")
fave, _ := h.deps.Faves.Create(userA.ID, "My secret", "", "", "private")
fave, _ := h.deps.Faves.Create(userA.ID, "My secret", "", "", "", "private")
tokenA, _ := sessions.Create(userA.ID)
cookieA := &http.Cookie{Name: "session", Value: tokenA}
@ -295,7 +295,7 @@ func TestAPIUpdateFave(t *testing.T) {
h, mux, users, sessions := testAPIServer(t)
user, _ := users.Create("testuser", "pass123", "user")
fave, _ := h.deps.Faves.Create(user.ID, "Original", "https://old.com", "", "public")
fave, _ := h.deps.Faves.Create(user.ID, "Original", "https://old.com", "", "", "public")
token, _ := sessions.Create(user.ID)
cookie := &http.Cookie{Name: "session", Value: token}
@ -322,7 +322,7 @@ func TestAPIUpdateFaveNotOwner(t *testing.T) {
h, mux, users, sessions := testAPIServer(t)
userA, _ := users.Create("usera", "pass123", "user")
fave, _ := h.deps.Faves.Create(userA.ID, "A's fave", "", "", "public")
fave, _ := h.deps.Faves.Create(userA.ID, "A's fave", "", "", "", "public")
cookieB := apiLogin(t, users, sessions, "userb", "pass123", "user")
@ -341,7 +341,7 @@ func TestAPIDeleteFave(t *testing.T) {
h, mux, users, sessions := testAPIServer(t)
user, _ := users.Create("testuser", "pass123", "user")
fave, _ := h.deps.Faves.Create(user.ID, "Delete me", "", "", "public")
fave, _ := h.deps.Faves.Create(user.ID, "Delete me", "", "", "", "public")
token, _ := sessions.Create(user.ID)
cookie := &http.Cookie{Name: "session", Value: token}
@ -368,7 +368,7 @@ func TestAPIDeleteFaveNotOwner(t *testing.T) {
h, mux, users, sessions := testAPIServer(t)
userA, _ := users.Create("usera", "pass123", "user")
fave, _ := h.deps.Faves.Create(userA.ID, "A's fave", "", "", "public")
fave, _ := h.deps.Faves.Create(userA.ID, "A's fave", "", "", "", "public")
cookieB := apiLogin(t, users, sessions, "userb", "pass123", "user")
@ -386,9 +386,9 @@ func TestAPIListFaves(t *testing.T) {
h, mux, users, sessions := testAPIServer(t)
user, _ := users.Create("testuser", "pass123", "user")
h.deps.Faves.Create(user.ID, "Fave 1", "", "", "public")
h.deps.Faves.Create(user.ID, "Fave 2", "", "", "public")
h.deps.Faves.Create(user.ID, "Fave 3", "", "", "private")
h.deps.Faves.Create(user.ID, "Fave 1", "", "", "", "public")
h.deps.Faves.Create(user.ID, "Fave 2", "", "", "", "public")
h.deps.Faves.Create(user.ID, "Fave 3", "", "", "", "private")
token, _ := sessions.Create(user.ID)
cookie := &http.Cookie{Name: "session", Value: token}
@ -413,7 +413,7 @@ func TestAPIListFavesPagination(t *testing.T) {
user, _ := users.Create("testuser", "pass123", "user")
for i := 0; i < 5; i++ {
h.deps.Faves.Create(user.ID, "Fave", "", "", "public")
h.deps.Faves.Create(user.ID, "Fave", "", "", "", "public")
}
token, _ := sessions.Create(user.ID)
cookie := &http.Cookie{Name: "session", Value: token}
@ -443,7 +443,7 @@ func TestAPISearchTags(t *testing.T) {
h, mux, users, _ := testAPIServer(t)
user, _ := users.Create("testuser", "pass123", "user")
fave, _ := h.deps.Faves.Create(user.ID, "Test", "", "", "public")
fave, _ := h.deps.Faves.Create(user.ID, "Test", "", "", "", "public")
h.deps.Tags.SetFaveTags(fave.ID, []string{"golang", "goroutines", "python"})
req := httptest.NewRequest("GET", "/api/v1/tags?q=go", nil)
@ -531,8 +531,8 @@ func TestAPIGetDisabledUser(t *testing.T) {
func TestAPIGetUserFaves(t *testing.T) {
h, mux, users, _ := testAPIServer(t)
user, _ := users.Create("testuser", "pass123", "user")
h.deps.Faves.Create(user.ID, "Public fave", "", "", "public")
h.deps.Faves.Create(user.ID, "Private fave", "", "", "private")
h.deps.Faves.Create(user.ID, "Public fave", "", "", "", "public")
h.deps.Faves.Create(user.ID, "Private fave", "", "", "", "private")
req := httptest.NewRequest("GET", "/api/v1/users/testuser/faves", nil)
rr := httptest.NewRecorder()
@ -555,8 +555,8 @@ func TestAPIExport(t *testing.T) {
h, mux, users, sessions := testAPIServer(t)
user, _ := users.Create("testuser", "pass123", "user")
h.deps.Faves.Create(user.ID, "Fave 1", "", "", "public")
h.deps.Faves.Create(user.ID, "Fave 2", "", "", "private")
h.deps.Faves.Create(user.ID, "Fave 1", "", "", "", "public")
h.deps.Faves.Create(user.ID, "Fave 2", "", "", "", "private")
token, _ := sessions.Create(user.ID)
cookie := &http.Cookie{Name: "session", Value: token}