Files
bookhoard/AGENTS.md
T
John O'Keefe 8de8883acf
Release / build-and-push (push) Successful in 2m55s
docs(agents): templ attribute-interpolation, settings-cache, collection-routing gotchas
From the smart-collections build: templ emits quoted attribute values
literally (whole-attribute expressions required — the rules page
shipped '/api/collections/{ collection.ID }' until proven via request
logs); SettingsRegistry only reloads through the settings API (direct
SQL writes look ignored); and user collections must never route through
the dashboard-section path (query_type='filter' is a schema DEFAULT,
not a query type — the 7ce7321 static-detail regression).
2026-10-14 12:00:33 -04:00

97 lines
5.0 KiB
Markdown

# AGENTS.md — Bookhoard server
Standing rules for any agent or developer working in this repo.
Project-wide context lives in the sibling repos' AGENTS.md files
(../bookhoard-app/AGENTS.md is the Android client's).
## Library visibility (family-lens rule) — ALWAYS apply
Per-user library visibility (`library_visibility` table, default-ALLOW:
every user sees every library unless an admin hides one) must be
enforced on **every endpoint that returns media metadata or content**.
It is NOT automatic — enforcement is per-handler.
- **New endpoints that list/filter media or series** MUST filter by the
requesting user's visible-library set, and MUST reject an explicit
`library_id` outside that set with 403 (never silently pass).
- Helpers (do not hand-roll):
- `handlers`: `(*MediaHandler).visibleLibrarySet` /
`(*SeriesHandler).visibleLibrarySet` — membership map + id slice.
- `router`: `visibleLibraryIDsFor` — same for SSR routes.
- `resolveLibrary` (helpers.go) only honors requested library ids
inside the visible set.
- SQL pattern: `AND (sqlc.narg('visible_library_ids')::uuid[] IS NULL OR
library_id = ANY(sqlc.narg('visible_library_ids')::uuid[]))` — see
`ListMediaItems`, `GetDistinctSeries`, `SearchMediaItems`.
- Content endpoints (file bytes, page images) additionally gate per item
like `ReaderHandler.ensureItemVisible` / the download endpoint.
- Tests must pin visibility: hidden library → 403 on the endpoint, and
absent from unscoped aggregates.
History: 2026-10-13 (`431fec0`) the series/media listing surfaces
leaked hidden libraries to any authenticated account — series endpoints
were name-only across ALL libraries. Fixed; do not regress.
## Tests
- Use `setupTestServer()` from `cmd/server/tests/test_helpers_test.go`
(see the Conventions section below for the pool/cleanup rules).
- Integration tests run against the SHARED dev database: never write
destructive SQL without preserving the dev rows (libraries Manga/
Comics/Ebooks, testuser's annotations and progress are real data).
- Test-created users must use `@tests.bookhoard.internal` or
`@example.com` emails and "test"-named collections/libraries so the
harness cleanup can find them.
- **NEVER rename or edit the shared admin (`testuser`) in tests** — a
renamed admin escapes the cleanup preserve-list and cascades real
data (this wiped the dev annotations once; see user_test.go
TestUpdateProfileSelfEdit for the dedicated-user pattern).
## Timezones
- The server owns display conversion: clients send instants (RFC3339),
the server renders in the requesting user's timezone.
- Use `utils.FormatTimeInTimezone` / `utils.UserLocation` — never
`time.Now().Format(...)` bare (that is server-UTC).
- Analytics day-buckets must bucket in the user's timezone
(`CreatedAt.In(loc)`), and date-range end bounds must be end-of-day.
## Known gotchas (do not relearn)
1. **templ does NOT interpolate `{ expr }` inside quoted attribute
values** — it emits the literal text. `x-init="f('{ col.ID }')"`
shipped a request to `/api/collections/{ collection.ID }` found by
the request logs; the whole attribute value must BE an expression:
`x-init={ "f('" + col.ID + "')" }`. Note
`class="theme-{ user.Theme }"` has always been literal (harmlessly
masked by the JS theme engine).
2. **SettingsRegistry caches**: writes must go through the settings
API (Reload on save). Direct SQL writes to system_settings leave
the cache stale until the next API-path write — never "preview"
rule changes via psql and expect them applied (2026-10-14: a live
curl test flip looked ignored until the API path was used).
3. **User collections are not dashboard sections**: nothing sets
collection show_on_dashboard for user rows, and GetDashboardSections
skips empty user sections. Collection detail (JSON + SSR) must NOT
route user rows through the dashboard-section path — a 7ce7321
regression did exactly that (static detail without library_id
returned zero books until 0a7fbd6). Only real system query types
(continue-reading etc.) route there; `query_type='filter'` is the
schema DEFAULT on every user row, not a query type.
## Conventions (absorbed from the retired PROJECT_GUIDELINES.md —
## only the rules that still matched reality)
- **Runtime is Docker** (Makefile auto-detects docker, falls back to
podman). The old "podman only" rule is dead.
- **The server binary is only built inside the Docker build**
(`make rebuild-app`); host-side Go builds are for tests/vet only.
- **Schema changes go in place** — edit `database/schema/schema.sql`,
never create migration files, until a release splits them.
- **Business logic lives in services**, not handlers; handlers parse
requests and call services (all DB access goes through services).
- **Integration tests**: one `setupTestServer()` per test function;
test pools use `max_conns=1`; never create a `pgxpool` per subtest
(connection exhaustion); use `t.Cleanup()`-based helpers; table-driven
subtests over duplicate functions.