diff --git a/.gitignore b/.gitignore index 7da7645..2b3721a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,10 @@ *.out /vendor/ -# IDE +# IDE / AI .vscode/ .idea/ +.claude/ *.swp *.swo *~ diff --git a/CLAUDE.md b/CLAUDE.md index 9519306..494a620 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,13 +14,24 @@ make test # Run tests make container # Build container image ``` +### CLI Admin Commands + +```bash +favoritter user list # List all users +favoritter user set-password # Change a user's password +favoritter user promote # Promote user to admin +favoritter user demote # Remove admin role +favoritter user lock # Disable user account +favoritter user unlock # Enable user account +``` + ## Architecture Single Go binary serving HTML (server-rendered templates + HTMX) and a JSON API. SQLite for storage, filesystem for uploaded images. All templates and static assets are embedded via `go:embed`. ### Directory Layout -- `cmd/favoritter/` — Entry point, wiring, graceful shutdown +- `cmd/favoritter/` — Entry point, wiring, graceful shutdown, CLI admin commands (`cli_*.go`) - `internal/config/` — Environment variable configuration - `internal/database/` — SQLite connection, PRAGMAs, migration runner - `internal/model/` — Domain types (no logic, no DB) @@ -37,7 +48,7 @@ Single Go binary serving HTML (server-rendered templates + HTMX) and a JSON API. ### Key Design Decisions - **Go 1.22+ stdlib router** — no framework, `http.ServeMux` with method routing -- **3 external dependencies** — modernc.org/sqlite (pure Go), golang.org/x/crypto (Argon2id), gorilla/feeds +- **Minimal dependencies** — modernc.org/sqlite (pure Go), golang.org/x/crypto (Argon2id), gorilla/feeds, google/uuid, golang.org/x/term - **`SetMaxOpenConns(1)`** — SQLite works best with a single writer; PRAGMAs are set once on the single connection - **Templates embedded in binary** — `//go:embed` for single-binary deployment; dev mode reads from disk for live reload - **Middleware chain order matters** — Recovery → SecurityHeaders → BasePath → RealIP → Logger → SessionLoader → CSRF → MustResetGuard diff --git a/Makefile b/Makefile index 2c64c94..fb02f84 100644 --- a/Makefile +++ b/Makefile @@ -29,24 +29,30 @@ build-all: $(DIST) ./cmd/favoritter; \ done -## Build .deb packages (requires nfpm) +## Build .deb packages (requires nfpm and envsubst) deb: build-all @for platform in $(PLATFORMS); do \ arch=$${platform##*/}; \ echo "Packaging deb for $$arch..."; \ - ARCH=$$arch VERSION=$(VERSION) nfpm package \ + ARCH=$$arch VERSION=$(VERSION) envsubst < nfpm.yaml > $(DIST)/nfpm-$$arch.yaml; \ + nfpm package \ + --config $(DIST)/nfpm-$$arch.yaml \ --packager deb \ --target $(DIST)/favoritter_$(VERSION)_$${arch}.deb; \ + rm -f $(DIST)/nfpm-$$arch.yaml; \ done -## Build .rpm packages (requires nfpm) +## Build .rpm packages (requires nfpm and envsubst) rpm: build-all @for platform in $(PLATFORMS); do \ arch=$${platform##*/}; \ echo "Packaging rpm for $$arch..."; \ - ARCH=$$arch VERSION=$(VERSION) nfpm package \ + ARCH=$$arch VERSION=$(VERSION) envsubst < nfpm.yaml > $(DIST)/nfpm-$$arch.yaml; \ + nfpm package \ + --config $(DIST)/nfpm-$$arch.yaml \ --packager rpm \ --target $(DIST)/favoritter_$(VERSION)_$${arch}.rpm; \ + rm -f $(DIST)/nfpm-$$arch.yaml; \ done ## Build container image diff --git a/PLANS-v1.1.md b/PLANS-v1.1.md new file mode 100644 index 0000000..369bd37 --- /dev/null +++ b/PLANS-v1.1.md @@ -0,0 +1,316 @@ +# Implementeringsplan v1.1 — Favoritter + +Fire funksjoner som utvider Favoritter med notater og OG-metadata, PWA-installering med Android-delingsstøtte, bedre redigeringsflyt, og utvidede admin-verktøy. + +## Oversikt + +| # | Funksjon | Omfang | Avhengigheter | +|---|----------|--------|---------------| +| 1 | OG-støtte + fritekstfelt (notes) | Migrasjon, modell, store, alle handlere, maler, eksport/import, feed | Ingen | +| 2 | PWA + Android share intent | Service worker, manifest, nye handlere, base-mal, JS | Avhenger av 1 (deling pre-fyller notes) | +| 3 | Redigeringsforbedringer (UX) | Maler, CSS, ny toggle-rute, ny partial | Avhenger av 1 (notes vises i kort) | +| 4 | Admin-forbedringer | Store, handlere, maler, ruter | Ingen | + +--- + +## Funksjon 1: OG-støtte + fritekstfelt (notes) + +### Beskrivelse + +Legger til et valgfritt langtekstfelt «notes» på hver favoritt. Forbedrer Open Graph-metatagger på detaljsiden slik at `og:description` bruker notes når det er tilgjengelig. + +### Berørte filer + +| Fil | Endring | +|-----|---------| +| `internal/database/migrations/002_add_fave_notes.sql` | Ny fil: ALTER TABLE faves ADD COLUMN notes | +| `internal/model/fave.go` | Legg til `Notes string` i Fave-struct | +| `internal/store/fave.go` | Oppdater alle SQL-spørringer, Create/Update-signaturer, scanFaves | +| `internal/handler/fave.go` | Les notes fra skjema i create/edit/update | +| `internal/handler/api/api.go` | Legg til notes i request/response-structs og faveJSON | +| `internal/handler/import_export.go` | Legg til notes i ExportFave, CSV-kolonner, import | +| `internal/handler/feed.go` | Bruk notes som feed item description | +| `web/templates/pages/fave_form.html` | Legg til textarea for notes mellom URL og bilde | +| `web/templates/pages/fave_detail.html` | Vis notes, forbedre og:description | + +### Migrasjons-SQL + +```sql +-- 002_add_fave_notes.sql +-- Legger til et valgfritt notatfelt på favoritter. +ALTER TABLE faves ADD COLUMN notes TEXT NOT NULL DEFAULT ''; +``` + +### Implementeringssteg + +1. **Migrasjon**: Opprett `internal/database/migrations/002_add_fave_notes.sql` +2. **Modell**: Legg til `Notes string` etter `ImagePath` i `model.Fave` +3. **Store — Create**: Endre signatur til `Create(userID, description, url, imagePath, notes, privacy)`, oppdater INSERT +4. **Store — GetByID**: Legg til `f.notes` i SELECT og `&f.Notes` i Scan +5. **Store — Update**: Endre signatur, legg til `notes = ?` i UPDATE +6. **Store — Alle list-metoder**: Legg til `f.notes` i SELECT for `ListByUser`, `ListPublicByUser`, `ListPublic`, `ListByTag` +7. **Store — scanFaves**: Legg til `&f.Notes` i Scan +8. **Handler — fave.go**: Les `notes` fra form i `handleFaveCreate` og `handleFaveUpdate`. Pass `fave.Notes` i `handleFaveEdit` template data +9. **Handler — api/api.go**: Legg til `Notes` i request/response-structs, `faveJSON`, import +10. **Handler — import_export.go**: Legg til notes i `ExportFave`, CSV header/rader, import-parsing +11. **Handler — feed.go**: Sett `item.Description = f.Notes` når det ikke er tomt i `favesToFeedItems` +12. **Template — fave_form.html**: Legg til ` + + diff --git a/web/templates/pages/fave_list.html b/web/templates/pages/fave_list.html index 155445f..814bd17 100644 --- a/web/templates/pages/fave_list.html +++ b/web/templates/pages/fave_list.html @@ -21,9 +21,6 @@ {{.Description}} - {{if eq .Privacy "private"}} - Privat - {{end}} {{if .Tags}}
@@ -32,6 +29,28 @@ {{end}}
{{end}} +
+ + + + Rediger + +
{{end}} diff --git a/web/templates/pages/home.html b/web/templates/pages/home.html index 9d28f9e..1515c85 100644 --- a/web/templates/pages/home.html +++ b/web/templates/pages/home.html @@ -1,3 +1,13 @@ +{{define "head"}} + + + +{{if .ExternalURL}} + +{{end}} + +{{end}} + {{define "content"}} {{if .User}}
diff --git a/web/templates/pages/profile.html b/web/templates/pages/profile.html index 0cf2f4b..ca25524 100644 --- a/web/templates/pages/profile.html +++ b/web/templates/pages/profile.html @@ -2,6 +2,11 @@ {{with .Data}}{{$d := .}}{{with .ProfileUser}} {{if eq .ProfileVisibility "public"}} + {{if .Bio}} + + {{else}} + + {{end}} {{if $.ExternalURL}} @@ -75,6 +80,20 @@ {{end}} {{end}} + {{if $d.IsOwner}} + + {{end}} {{end}} diff --git a/web/templates/pages/tag_browse.html b/web/templates/pages/tag_browse.html index 2502082..84cfaa9 100644 --- a/web/templates/pages/tag_browse.html +++ b/web/templates/pages/tag_browse.html @@ -1,3 +1,15 @@ +{{define "head"}} +{{with .Data}} + + + +{{if $.ExternalURL}} + +{{end}} + +{{end}} +{{end}} + {{define "content"}} {{with .Data}}
diff --git a/web/templates/partials/privacy_toggle.html b/web/templates/partials/privacy_toggle.html new file mode 100644 index 0000000..90a0b9f --- /dev/null +++ b/web/templates/partials/privacy_toggle.html @@ -0,0 +1,10 @@ + + + diff --git a/web/templates/partials/tag_suggestions.html b/web/templates/partials/tag_suggestions.html index 22911e1..1b833b6 100644 --- a/web/templates/partials/tag_suggestions.html +++ b/web/templates/partials/tag_suggestions.html @@ -2,6 +2,6 @@ id="tag-option-{{$i}}" class="tag-suggestion" aria-selected="false" - onclick="addTag(this, '{{$tag.Name}}')" + data-tag-name="{{$tag.Name}}" tabindex="-1">{{$tag.Name}} {{end}}