Release / build-and-push (push) Successful in 2m55s
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).
5.0 KiB
5.0 KiB
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_idoutside 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[]))— seeListMediaItems,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()fromcmd/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.internalor@example.comemails 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— nevertime.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)
- 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 + "')" }. Noteclass="theme-{ user.Theme }"has always been literal (harmlessly masked by the JS theme engine). - 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).
- 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
7ce7321regression did exactly that (static detail without library_id returned zero books until0a7fbd6). 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 usemax_conns=1; never create apgxpoolper subtest (connection exhaustion); uset.Cleanup()-based helpers; table-driven subtests over duplicate functions.