Gitea runs jobs in containers, so service containers are networked by
name (same as GitHub Actions with container:). Postgres goes back into
services: and is addressed as 'postgres', not localhost. MinIO stays
as a manual docker run with --network container:$(hostname) since it
needs `server /data` and is addressed as localhost.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Service containers bind ports to the host, not to localhost inside the
job container. Start both Postgres and MinIO manually with
--network container:$(hostname) so they share the job container's
network namespace and are reachable on localhost. Use docker exec for
pg_isready to avoid depending on postgresql-client in the runner image.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Alembic scaffolds migration files from its own template which uses
pre-3.10 conventions (Union[X, Y], typing.Sequence, etc). Excluding
avoids noise on every new migration without affecting app code coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Install uv via official installer script instead of pip (pip not
available in the runner environment). Add ~/.local/bin to GITHUB_PATH
so uv is on PATH for subsequent steps.
MinIO: replace -p 9000:9000 (binds to host, unreachable from job
container) with --network container:$(hostname) which joins MinIO to
the job container's network namespace, making localhost:9000 resolve
correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gitea Actions execs JavaScript actions (actions/checkout) inside the
job container, unlike GitHub Actions which uses the host. The uv Python
image has no Node.js, causing exit 127. Fix: drop container: from all
three API jobs and run on the default ubuntu-latest environment.
Integration tests: Postgres stays as a service container (no special
startup command needed). MinIO moved to a manual docker run step using
quay.io/minio/minio with `server /data` — the only way to pass a
startup command. Bucket created via mc binary downloaded in-step.
Service hostnames change from service-name to localhost now that there
is no explicit job container.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unused NEVER import from detail.component.spec.ts
- Replace `null as unknown as ImageRecord` with `null as unknown as typeof MOCK_IMAGE`
to match the narrower inferred type (thumbnail_key: null) that setup() expects
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five test/lint jobs run on every push to master and every PR:
- ui-test: Karma/Firefox in node:22-bullseye
- ui-lint: ESLint via ng lint
- api-unit: pytest tests/unit/ via uv in Python 3.12
- api-lint: Ruff via uvx (no dep install needed)
- api-integration: pytest tests/integration/ with Postgres 16 and bitnami/minio services
Build jobs (build-api, build-ui) run only on v* tags and are gated
behind all five test/lint jobs passing. Images pushed to $REGISTRY.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LoginComponent: provide ActivatedRoute stub (component reads returnUrl query param)
- UploadComponent: add cdr.markForCheck() to handleUploadError so OnPush view updates
when the method is called directly; fix success test to check showSuccess not toastMessage
- DetailComponent: drive not-found-card and tag-error tests through component methods
that call markForCheck() rather than directly mutating state on OnPush components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds « ‹ [1][2][3][4] › » navigation to the library. Page window
slides to keep the current page in view. Prev/next/first/last controls
are always rendered but disabled at their respective bounds. Also wires
up karmaConfig in angular.json so FirefoxHeadless is used for tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dockerfile.prod explicitly listed copied directories and omitted
scripts/, so the migration script was absent from the prod image.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Short IDs become the canonical identifier in URLs (/i/:short_id),
MinIO/R2 storage keys, and all API responses. Hash-based deduplication
is preserved. Includes two-phase Alembic migration (003 adds nullable
column, 004 enforces NOT NULL) with a backfill script to copy storage
objects and populate short_id for existing images.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Changes prefix-only LIKE to case-insensitive ILIKE with leading
wildcard so queries like "at" now match "cat", "scatter", etc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The .catch() handler on a rejected promise resolves on the second
microtask tick, not the first — one extra await Promise.resolve() is
needed before the assertion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Detail page now has a "Copy URL" button that copies the image's direct
file URL to the clipboard. A toast service (BehaviorSubject-backed,
auto-dismissing after 3s) confirms success or failure. ToastComponent
is registered at the app root and available to all future features.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking the Reactbin home link (or any navigation to / that removes
?page=) now resets the displayed page by subscribing to queryParamMap
for post-init URL changes. Cards with many tags no longer push the
pagination bar down since the tag row is clamped to one line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Page size changes from 50 to 24. Library now shows discrete page navigation
with a "Page N of M" indicator, total image count, and URL state (?page=N)
so pages are bookmarkable and the browser Back button works. Tag filter
resets to page 1. Out-of-range page params are clamped silently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
XFF[0] is attacker-controllable; a crafted X-Forwarded-For header could
attribute login failures to a victim IP, triggering their lockout while
the attacker accumulates none. ingress-nginx sets X-Real-IP via its
realip module using an authoritative CIDR allowlist and overwrites any
client-supplied value, making it spoof-resistant. Fallback to XFF[0]
is retained for defence in depth but now emits a warning if reached.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
API responses now include file_url and thumbnail_url fields. When
S3_PUBLIC_BASE_URL is configured, these point to the CDN domain;
when unset, they fall back to the existing API proxy paths so local
dev requires no additional setup. UI updated to use response URL
fields directly instead of constructing proxy URLs client-side.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Default client_max_body_size of 1MB was rejecting uploads larger than 1MB
with a 413 before the request reached the API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Aligns principles with actual project state: soften TDD wording to allow
tests alongside implementation, replace CI gate with concrete local test
suite gate, add production infrastructure to tech stack (k3s, nginx,
Vault + VSO), and document plaintext password storage as a known gap
that must be resolved before further auth work.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds complete k8s/ manifest tree: Namespace, VaultAuth + VaultStaticSecret
CRDs (VSO secret sync from Vault KV v2), API and UI Deployments and Services,
nginx Ingress with cert-manager TLS, MinIO StatefulSet with PVC and init Job,
and Alembic init container on the API Deployment for automatic schema
migrations. Includes .yamllint.yml config and validate-k8s Makefile target.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>