/api/books/match returned media metadata from ALL libraries regardless
of visibility: the resolver lookups (GetMediaItemBySHA256 and friends)
are global, so match results leaked hidden-library titles, covers and
ids to any authenticated user, and LinkBook accepted a link target from
a hidden library. Both now enforce the family-lens rule:
- QueryBooks post-filters the cascade by the requester's visible-library
set (absent-from-results — a hidden library's book simply never
matches), before determineAction so the suggested action reflects the
filtered set. BookQueryRequest gains a server-side-only
VisibleLibraryIDs field; nil keeps legacy callers unfiltered.
- LinkBook verifies the device belongs to the caller
(DeviceExistsForUser) and the target item's library is visible —
403 otherwise. Linking was previously unscoped on both axes.
- GetUnlinkedBooks likewise verifies device ownership (its service is
still a stub, but the route no longer answers for other users'
devices).
Also: the collections books-grid partial uses a light loader —
requested library validated against the visible set directly instead of
the page-only library resolution (folder reachability, counts,
dropdown payload), removing the extra queries that made library
switching hold the spinner longer.
And the dashboard library switch now folds the This Week widget's
htmx refresh behind the spinner (refreshWeekWidget awaits the widget's
swap, with a safety timeout) instead of letting it pop in after the
fade-in — the two-stage update introduced with the widget's
library-scoped re-fetch.
Guardrail: the rendering lint now counts all innerHTML assignments (not
just template-literal ones) — the old pattern undercounted 20 files at
80 sites, including .join("") builders like renderDashboardCollections.
Baseline rebalanced accordingly.
204 lines
8.5 KiB
Makefile
204 lines
8.5 KiB
Makefile
.PHONY: help test test-integration test-all rebuild-app rebuild-app-force rebuild rebuild-force rebuild-force-db clean restart up down logs ps test-env-up test-env-down release
|
|
|
|
# Include .env file for environment variables (single source of truth)
|
|
# Ignore if .env doesn't exist yet
|
|
ifneq (,$(wildcard ./.env))
|
|
include .env
|
|
export
|
|
endif
|
|
|
|
# Auto-detect container runtime: prefer docker, fall back to podman
|
|
# Override with: CONTAINER_RUNTIME=podman make rebuild-app
|
|
CONTAINER_RUNTIME ?= $(shell command -v docker 2>/dev/null || command -v podman 2>/dev/null)
|
|
|
|
# Dev compose stack: base prod file merged with the dev override (local build + tests).
|
|
# Prod deploy does NOT use this — it runs plain `docker compose` against the base file only.
|
|
COMPOSE := $(CONTAINER_RUNTIME) compose -f docker-compose.yml -f docker-compose.dev.yml
|
|
|
|
# Default target
|
|
help:
|
|
@echo "Available targets:"
|
|
@echo ""
|
|
@echo "Development:"
|
|
@echo " make up - Start containers (if already built)"
|
|
@echo " make rebuild - Rebuild all containers (database preserved)"
|
|
@echo " make rebuild-force - Rebuild all containers, no cache (database preserved)"
|
|
@echo " make rebuild-app - Rebuild app container only (fast, database preserved)"
|
|
@echo " make rebuild-app-force - Rebuild app container only, no cache (database preserved)"
|
|
@echo " make rebuild-force-db - Rebuild all containers, no cache (DELETES database)"
|
|
@echo " make restart - Restart app container (preserves db)"
|
|
@echo " make down - Stop all containers (preserves volumes)"
|
|
@echo " make clean - Stop and remove all containers and volumes"
|
|
@echo ""
|
|
@echo "Status & Logs:"
|
|
@echo " make ps - Show container status"
|
|
@echo " make logs - Show container logs (follow mode)"
|
|
@echo ""
|
|
@echo "Testing:"
|
|
@echo " make test - Run unit tests (from host, fast)"
|
|
@echo " make test-integration - Run integration tests (in containers, matches prod)"
|
|
@echo " make test-all - Run all tests (unit + integration)"
|
|
@echo " make test-env-up - Start containers with test mode enabled"
|
|
@echo " make test-env-down - Stop test environment"
|
|
@echo ""
|
|
@echo "Verification:"
|
|
@echo ""
|
|
@echo "Release:"
|
|
@echo " ./release v0.3.0 - Tag, push, and release (notes auto-generated from commits)"
|
|
|
|
# Run unit tests locally (fast, no containers)
|
|
test: lint-rendering
|
|
go test ./internal/... -v -short
|
|
|
|
# Rendering-ownership guard (AGENTS.md → Rendering ownership): templ renders
|
|
# UI markup, JS orchestrates. Fails when a web/src file builds more HTML
|
|
# template literals than its recorded baseline (web/src/rendering-allowlist.txt),
|
|
# or when a .templ file gains an inline <script> block (theme_flash_guard.templ
|
|
# is the only sanctioned one). Shrink baselines as fragments move to templ
|
|
# partials; grow one only with a written reason.
|
|
lint-rendering:
|
|
@violations=""; \
|
|
for f in $$(grep -rlE 'innerHTML[[:space:]]*=[[:space:]]*' web/src --include='*.ts' | grep -v 'web/src/reader/'); do \
|
|
count=$$(grep -E 'innerHTML[[:space:]]*=[[:space:]]*' $$f | grep -cvE 'innerHTML[[:space:]]*=[[:space:]]*""[[:space:]]*$$'); \
|
|
allowed=$$(grep -F "$$f:" web/src/rendering-allowlist.txt | cut -d: -f2); \
|
|
allowed=$${allowed:-0}; \
|
|
if [ $$count -gt $$allowed ]; then \
|
|
violations="$$violations $$f ($$count > $$allowed)"; \
|
|
fi; \
|
|
done; \
|
|
for f in $$(grep -rln '<script>' templates --include='*.templ'); do \
|
|
case $$f in *theme_flash_guard*) continue;; esac; \
|
|
violations="$$violations $$f (inline script)"; \
|
|
done; \
|
|
if [ -n "$$violations" ]; then \
|
|
echo "rendering-ownership violations:$$violations"; exit 1; \
|
|
fi; \
|
|
echo "rendering ownership OK"
|
|
|
|
# Run integration tests in containers (matches production environment)
|
|
test-integration:
|
|
@echo "Building test containers..."
|
|
$(COMPOSE) --profile tests build
|
|
@echo "Starting application containers..."
|
|
$(COMPOSE) up -d db app
|
|
@echo "Waiting for services to be healthy..."
|
|
@until $(CONTAINER_RUNTIME) exec bookhoard_db pg_isready -U postgres > /dev/null 2>&1; do \
|
|
echo " Database not ready yet..."; \
|
|
sleep 2; \
|
|
done; \
|
|
echo " ✓ Database is ready"
|
|
@until $(CONTAINER_RUNTIME) exec bookhoard curl -sf http://localhost:8765/health > /dev/null 2>&1; do \
|
|
echo " Application not ready yet..."; \
|
|
sleep 2; \
|
|
done; \
|
|
echo " ✓ Application is ready"
|
|
@echo ""
|
|
@echo "Running integration tests in container..."
|
|
$(COMPOSE) --profile tests run --rm tests
|
|
@echo ""
|
|
@echo "✅ Integration tests completed!"
|
|
@echo "📝 Containers are still running. Use 'make logs' to view logs or 'make clean' to stop."
|
|
|
|
# Run all tests
|
|
test-all: test test-integration
|
|
|
|
# Rebuild app container only (preserve DB, with cache)
|
|
rebuild-app:
|
|
@echo "Rebuilding app container (database stays running)..."
|
|
$(COMPOSE) up --build --force-recreate -d app
|
|
@echo "✓ App container rebuilt and restarted"
|
|
|
|
# Rebuild app container only (preserve DB, no cache)
|
|
rebuild-app-force:
|
|
@echo "Force rebuilding app container (database stays running, no cache)..."
|
|
$(COMPOSE) build --no-cache app
|
|
$(COMPOSE) up --force-recreate -d app
|
|
@echo "✓ App container rebuilt and restarted"
|
|
|
|
# Rebuild all containers (preserve DB, with cache)
|
|
rebuild:
|
|
@echo "Rebuilding all containers (database preserved)..."
|
|
$(COMPOSE) up --build --force-recreate -d
|
|
@echo "✓ All containers rebuilt and restarted"
|
|
|
|
# Rebuild all containers (preserve DB, no cache)
|
|
rebuild-force:
|
|
@echo "Force rebuilding all containers (database preserved, no cache)..."
|
|
$(COMPOSE) build --no-cache
|
|
$(COMPOSE) up --force-recreate -d
|
|
@echo "✓ All containers rebuilt and restarted"
|
|
|
|
# Rebuild all containers (remove DB, no cache)
|
|
rebuild-force-db:
|
|
@echo "Force rebuilding all containers (database will be DELETED, no cache)..."
|
|
$(COMPOSE) down -v
|
|
$(COMPOSE) build --no-cache
|
|
$(COMPOSE) up --force-recreate -d
|
|
@echo "✓ All containers rebuilt and restarted"
|
|
|
|
# Stop and remove containers
|
|
clean:
|
|
$(COMPOSE) down -v
|
|
|
|
# Quick start (if already built)
|
|
up:
|
|
$(COMPOSE) up -d
|
|
|
|
# Stop all containers (alias for clean)
|
|
down:
|
|
$(COMPOSE) down
|
|
|
|
# Restart app container (preserves database)
|
|
restart:
|
|
@echo "Restarting app container (database stays running)..."
|
|
$(COMPOSE) restart app
|
|
@echo "✓ App container restarted"
|
|
|
|
# Show container status
|
|
ps:
|
|
$(COMPOSE) ps
|
|
|
|
# Show container logs
|
|
logs:
|
|
$(COMPOSE) logs -f
|
|
|
|
# Start containers with test mode enabled for manual testing
|
|
test-env-up:
|
|
@echo "Starting containers with test mode enabled..."
|
|
TEST_MODE=true RATE_LIMIT_ENABLED=false REQUESTS_PER_MINUTE=1000 $(COMPOSE) up --build --force-recreate -d
|
|
@echo "Waiting for services to be ready..."
|
|
@until $(CONTAINER_RUNTIME) exec bookhoard_db pg_isready -U postgres > /dev/null 2>&1; do sleep 1; done
|
|
@until $(CONTAINER_RUNTIME) exec bookhoard curl -sf http://localhost:8765/health > /dev/null 2>&1; do sleep 1; done
|
|
@echo "✓ Test environment is ready!"
|
|
@echo "Application available at http://localhost:8765"
|
|
|
|
# Stop test environment
|
|
test-env-down:
|
|
$(COMPOSE) down -v
|
|
|
|
|
|
|
|
# Create an annotated version tag carrying auto-generated release notes (git-cliff)
|
|
# and push it. The tag push triggers .gitea/workflows/release.yml, which builds the
|
|
# image and publishes a Gitea Release whose body is this tag's message. Notes come
|
|
# entirely from Conventional Commits — no hand-written message required.
|
|
#
|
|
# git-cliff's --latest needs the tag to exist to scope the notes, so we create a
|
|
# throwaway lightweight tag, generate the notes, replace it with an annotated tag,
|
|
# then push. --cleanup=verbatim keeps the markdown "###" group headers (git's
|
|
# default cleanup would strip lines starting with "#").
|
|
#
|
|
# Requires git-cliff: https://git-cliff.org/install
|
|
# Usage: make release VERSION=v0.3.0
|
|
release:
|
|
@test -n "$(VERSION)" || { echo "Usage: make release VERSION=v0.3.0"; exit 1; }
|
|
@command -v git-cliff >/dev/null 2>&1 || { echo "git-cliff not found — install: https://git-cliff.org/install"; exit 1; }
|
|
@if git rev-parse "$(VERSION)" >/dev/null 2>&1; then echo "Tag $(VERSION) already exists locally — delete it first: git tag -d $(VERSION)"; exit 1; fi
|
|
@echo "Generating release notes for $(VERSION)..."
|
|
@git tag "$(VERSION)" HEAD && \
|
|
(git cliff --latest --config cliff.toml > .release-notes.tmp && git tag -d "$(VERSION)" >/dev/null) || \
|
|
{ git tag -d "$(VERSION)" >/dev/null 2>&1; rm -f .release-notes.tmp; echo "git-cliff failed"; exit 1; }
|
|
@git tag -a --cleanup=verbatim -F .release-notes.tmp "$(VERSION)" HEAD && rm -f .release-notes.tmp
|
|
@git push origin "$(VERSION)"
|
|
@echo "Pushed $(VERSION) — Gitea Actions will build the image and publish the Release."
|