Appearance
Console Operations
Restricted inventory
Keep actual origins, addresses, private paths, staff emails and secrets outside this repository. Resolve the following operator variables from the restricted inventory before using commands:
| Variable | Meaning |
|---|---|
ADMIN_PROJECT, ADMIN_COMPOSE_FILE, ADMIN_SERVICE | Canonical console Compose deployment |
ADMIN_ENV_FILE, ADMIN_CONTAINER | Protected environment and running console |
PUBLIC_CADDY_CONTAINER, PUBLIC_CADDYFILE | Public ingress and host-side configuration |
CADDY_CONFIG_IN_CONTAINER | Mounted ingress configuration |
DB_CONTAINER, DB_OWNER, DB_NAME | Correct PostgreSQL target and owner |
BACKUP_ROOT, RELEASE_DIR, PREVIOUS_IMAGE, NEXT_IMAGE | Protected backups and reviewed images |
ADMIN_ORIGIN, ADMIN_FQDN, MOBILE_API_HEALTH_ENDPOINT | Approved endpoint variables |
CONSOLE_PRIVATE_IP, CONSOLE_PRIVATE_PORT, ACTUAL_PROXY_SOURCE_IP | Verified private connectivity |
Do not substitute a guessed path, project or database. Environment files contain secrets; never display them with cat, include them in Git or post full Compose-expanded configuration.
Backups before changes
Obtain approval for the specific migration/restart. Enter an authorised root shell successfully before protected file operations. A failed sudo prompt followed by permission errors is not a backup.
bash
umask 077
: "${BACKUP_ROOT:?}" "${ADMIN_ENV_FILE:?}" "${ADMIN_COMPOSE_FILE:?}" "${PUBLIC_CADDYFILE:?}"
BACKUP_DIR="$BACKUP_ROOT/admin-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p -- "$BACKUP_DIR"
cp -- "$ADMIN_ENV_FILE" "$BACKUP_DIR/console.env"
cp -- "$ADMIN_COMPOSE_FILE" "$BACKUP_DIR/compose.yml"
cp -- "$PUBLIC_CADDYFILE" "$BACKUP_DIR/Caddyfile"
docker exec "$DB_CONTAINER" pg_dump -U "$DB_OWNER" -d "$DB_NAME" \
--format=custom --schema=admin_console > "$BACKUP_DIR/admin-schema.dump"
test -s "$BACKUP_DIR/admin-schema.dump"
docker exec -i "$DB_CONTAINER" pg_restore --list < "$BACKUP_DIR/admin-schema.dump"
chmod 600 "$BACKUP_DIR"/*Capture the current image/commit and restricted deployment inventory separately. A schema dump includes staff hashes and sessions; protect it as sensitive data. Listing a dump verifies readability, not a full restore rehearsal. Asset publication additionally needs the applicable image/proposal backups. A Git tag alone cannot restore database contents, images or configuration.
Migration ledger
| Installation | Purpose |
|---|---|
schema.sql | Initial namespace, staff, sessions, catalogue, media and reader/publisher roles |
002-rarity-reference.sql | Read-only Benchmark snapshot |
003-scan-quota.sql | Scoped daily scan support |
004-super-admin.sql | SUPER_ADMIN, access audit, last-owner protection and session revocation |
005-mobile-accounts.sql | Scoped mobile support; requires corresponding backend account-control schema |
006-scan-statistics.sql | Independent scan outcome views; requires corresponding backend statistics schema |
007-card-metadata.sql | Read-only full catalogue metadata |
008-staff-password.sql | Own-password capability, audit and password-version invalidation |
009-multi-tcg.sql | Expand catalogue/metadata read views to all stored TCGs |
010-sealed-catalogue.sql | Restricted sealed-product, classification and type read views |
013-user-collection.sql | SUPER_ADMIN-only private holding reads and restricted consultation journal |
Inspect migration SQL and dependencies first. Apply only the reviewed required migration, with ON_ERROR_STOP and bounded lock/statement timeouts. The console never runs migrations on boot. Do not give the runtime database owner privileges to avoid an installation failure.
Console release
- Merge the reviewed PR only after relevant tests and image build pass.
- Build from the merged commit, not an uncommitted developer checkout.
- Back up configuration/schema and validate the dump.
- Apply the approved additive migration.
- Change only the canonical console image to the reviewed release; retain the existing origin, secrets, proxy trust and media mounts.
- Recreate only the console, then check health and authentication.
bash
docker compose -p "$ADMIN_PROJECT" -f "$ADMIN_COMPOSE_FILE" \
up -d --no-deps --wait --wait-timeout 120 "$ADMIN_SERVICE"An environment change needs recreation, not a plain container restart. Check for stale image overrides before deploying. This procedure does not restart PostgreSQL, the mobile API, scan workers or pricing services. Keep both media paths separate and inspect private network membership.
Public activation
The domain owner creates the dedicated DNS entry. Check authoritative resolution and public resolver convergence, including unintended IPv6 entries, without altering mail records. DNS caching can delay access.
The technical operator configures the exact public ADMIN_ORIGIN, verifies secure cookies and the actual proxy source, and adds an independent top-level Caddy host block forwarding only to the private console listener. Preserve the existing mobile/API host block. The root may redirect to the admin login.
Caddy already provides automated TLS on the ingress. Do not add another web server, duplicate certificate manager, self-signed public certificate or browser TLS bypass. Persistent certificate storage and ingress challenge reachability must remain intact.
In Nano, append the new block outside the existing host block, save with Ctrl+O then Enter, and exit with Ctrl+X. Validate the actual mounted configuration before any reload:
bash
docker exec "$PUBLIC_CADDY_CONTAINER" caddy validate \
--config "$CADDY_CONFIG_IN_CONTAINER" --adapter caddyfileReload with the control API disabled
The deployed Caddy had admin off; therefore caddy reload failed with connection refused to the loopback control port. This did not mean the public listener or console was unhealthy. Keep the control API disabled.
For Caddy 2.11 or later started with caddy run --config ... --adapter caddyfile, without --resume or later API configuration changes, the supported file reload signal is:
bash
docker kill --signal=SIGUSR1 "$PUBLIC_CADDY_CONTAINER"Despite the Docker command name, this sends the specified reload signal, not the default kill signal. Validate first. A successful signal command alone is not proof of a successful reload: inspect bounded ingress logs and request the endpoints. The recorded deployment used Caddy 2.11.4 and this method succeeded. On other startup modes/versions, investigate supported reload behaviour; an ingress restart needs separate approval because it can interrupt mobile traffic.
Validation
Catalogue image containment and staff access
The 2026-10-14 emergency change replaced anonymous catalogue-image forwarding in the existing mobile host block with this scoped Caddy handler:
text
handle /api/images/* {
header Cache-Control "private, no-store"
respond "Authentication required" 401
}This is deployment configuration, not a database migration. Keep the real host block, mounted file paths and backup location in the restricted inventory. Before editing, obtain approval and copy the host-side Caddyfile into a timestamped, root-protected backup. Replace only the catalogue-image handler; preserve API, private-route restrictions, TLS storage, admin and Grafana routing. Validate the mounted file and use the supported reload procedure above.
The first containment intentionally affected old mobile builds and the console's former cross-origin image URLs. Admin frontend PR 23 subsequently restored staff viewing through same-origin session-protected routes. Documentation PR 216 records the role and media boundaries. The console deployment changed its image only, after configuration backup; no SQL migration, source-image mutation or mobile API/worker restart was needed.
Verify this matrix before calling the access change complete:
| Request | Expected result |
|---|---|
Old mobile /api/images/... URL, without credentials | 401 |
| Console catalogue-image URL, without a staff session | 401 |
| Console catalogue-image URL, active MARKETING session | 403 |
| Valid record image, active ADMIN or SUPER_ADMIN session | 200 |
| Wrong record, traversal or escaping symlink | Refused; no file bytes |
| Revoked staff session | 401 |
| Existing mobile API health endpoint | Healthy |
Use disposable local fixtures for role/browser tests; do not create production staff for deployment verification. Reload an already authenticated console tab to receive its new image URLs. Copying those URLs into an unauthenticated browser must not grant access. Staff cookies belong to the console origin and are not mobile credentials.
Mobile follow-up deployed on 2026-10-15: backend PR 478 and mobile PR 668 provide guest catalogue artwork through /api/catalogue-media/images/*. Only known catalogue paths pass database checks; sealed artwork additionally requires reviewed, mapped products and public rights. Private media and console staff-session requirements are unchanged. Raw /api/images/* remains denied; old mobile builds must update their image URLs. Guest card, logo and sealed requests were verified with HTTP 200 and raw image requests with HTTP 401. See the rollout record. Published catalogue artwork is intentionally public, including to browsers; an app User-Agent or shared client secret is not authentication. Do not reopen the raw image handler to recover old-build visuals.
A console rollback must retain the public image block. Rolling back staff image delivery can break previews again; this is preferable to silently reopening anonymous access. Already downloaded/publicly cached copies cannot be recalled.
bash
curl -I "$ADMIN_ORIGIN/admin/login"
curl -I "$ADMIN_ORIGIN/admin"
curl -fsS "$MOBILE_API_HEALTH_ENDPOINT"Expect login success over valid TLS and an anonymous dashboard redirect/refusal. Perform a real staff browser login and verify the expected role. For password releases, use a disposable staff fixture to test old/new credentials and revoked sessions; do not rotate a real colleague's password merely to test deployment. Remove/disable a technical fixture through an approved scoped procedure.
Local checks include unit/HTTP/database tests, reader permission tests, image build and Playwright checks on desktop/mobile in both themes. Password tests cover wrong current password, confirmation, target injection, stale hash, inactive accounts, SQL grants, CSRF, old session replay and unchanged mobile identities.
Private collection rollout
Admin frontend PR 28 adds Comptes de l'app > Collection for SUPER_ADMIN only. It requires explicit installation of 013-user-collection.sql after approval and an admin-schema/configuration backup. The function reads existing holding and catalogue columns; it changes no customer, holding, price or asset records. Only the new consultation journal is written by reads. The migration is repeatable and never runs automatically on console boot.
Keep private-table access behind the function. Do not grant the reader generic SELECT or UPDATE on public.collection_items, nor read/write access to the consultation journal. The operator installs SQL with bounded lock/statement timeouts and ON_ERROR_STOP. Deploy/recreate only the console after installation; do not restart the database, mobile API, pricing workers or scanner services.
Validate SUPER_ADMIN access, anonymous 401, ADMIN/MARKETING 403, Origin rejection, revoked sessions, target isolation, excluded deleted entries, pagination, private image loading and unchanged holdings. Verify journaling without logging private contents or creating production test staff. Local checks are npm test and npm run test:browser:collections, including desktop/mobile in both themes.
Rollback the reviewed console image only if needed; the additive function and journal can remain installed. Dropping that journal or restoring the entire database requires separate approval. Record rollout evidence privately and do not call the feature deployed until migration and console verification succeed.
Account operations
Use the existing interactive staff command with an operator-only STAFF_DATABASE_URL. Choose an action intentionally; creation, password rotation and disabling are separate operations. Do not use runtime reader credentials for provisioning and do not place a password in a shell argument. Deliver temporary credentials privately. SUPER_ADMIN should be exceptional; marketing ordinarily needs MARKETING.
The personal change flow is Mon compte -> Changer mon mot de passe -> fresh login. It preserves the role and requires no mobile identity mutation. Staff access controls cannot demote/remove the last active SUPER_ADMIN.
Rollback
Record previous image/configuration before release. Restore only the authorised console image/configuration and recreate only that service. An additive SQL capability can remain installed if harmless and compatible; reverting migrations or restoring database backups is a separate reviewed operation, not an automatic deploy rollback. In particular, an old image can ignore password-version protection: assess that security regression before downgrading.
For ingress rollback, restore the reviewed configuration, validate it and use the same supported reload method. Preserve TLS storage. For media, use the audited proposal restore only when no later publication conflicts. Never blanket-delete catalogue files or staff tables.
Troubleshooting
| Symptom | Check / response |
|---|---|
| Permission denied copying configuration | Confirm privileged shell succeeded; do not claim a backup was made |
| Docker socket permission denied | Approved root access or refreshed authorised Docker group session; Docker access is root-equivalent |
| Login page unavailable on local fallback | Check tunnel/gateway separately from public ingress |
| Private certificate warning | Trust only the verified private CA in the intended user store; never distribute its private key |
| Public DNS incorrect | Check authoritative records, cache and IPv6 before modifying the ingress |
| Configuration valid, formatting warning | Formatting is not a syntax failure; format intentionally, then validate again |
| Control-port connection refused | admin off is intentional; verify file-based signal conditions |
| 502 on public console | Check private console address, listener, health and database connectivity |
| Login/POST rejected after public activation | Verify exact Origin and proxy trust; recreate environment changes |
| Secure cookie missing / login loop | Check HTTPS termination and trusted proxy source, not global trust |
| 429 for colleagues | Check per-IP/shared-network and per-account limits; do not remove protection blindly |
| Missing custom views | Check generated AdminJS component bundle; do not serve arbitrary dotfiles |
| Missing catalogue properties | Verify migration 007 and view grants; absent source values remain absent |
| Password login fails after release | Existing sessions need fresh login; migration 008 must precede the new image |
| Role change invalidates session | Expected security behaviour; reconnect with current permissions |
| Stale support confirmation | Refresh account revision/day/counter before retrying |
| New image but same scan result | Catalogue publication does not automatically rebuild scanner indexes |
| Private gateway health/PID issue | Investigate that container independently; public success does not prove its recovery |
Observability integration
Configure ADMIN_GRAFANA_URL with the approved credential-free HTTPS base URL accessible from colleagues' browsers. Include the Grafana subpath. Never configure a localhost URL for a shared browser link. ADMIN and SUPER_ADMIN can access these links; no business-table migration is required.
Public activation was explicitly approved with Grafana authentication retained. Use the existing ingress/domain and a dedicated path rather than creating new DNS or exposing Loki. Back up the public Caddy configuration, console environment/Compose configuration and Grafana deployment/database first. A stopped Grafana database backup avoids an inconsistent live SQLite/WAL copy. Preserve its secrets and volume.
Set Grafana's external root URL with the subpath, enable serving from that subpath and secure cookies. Keep anonymous access and self-signup disabled, the Grafana host port bound only to loopback and existing authentication unchanged. Caddy must preserve the prefix when Grafana serves it itself and support its WebSocket route. Do not enable auth-proxy headers or bypass Grafana login.
If a dedicated Compose override is used, it becomes part of the canonical Grafana deployment command. Record both the base and override paths in the private operator inventory; recreating with the base alone would restore the old root URL. Update only Grafana, not Loki or Alloy. Recreate the console for its URL environment change, validate the ingress file and reload it through the supported signal procedure above.
Validate the login page, JS/CSS assets, subpath redirects and unauthenticated dashboard/search/datasource rejection. Confirm existing console login and mobile health. Use no synthetic production staff for verification. Actual Grafana logins remain a human verification step unless an authorised technical credential already exists and is used without exposure.
Rollback restores the saved ingress/console configuration and prior Grafana deployment configuration, then recreates only those approved services. Existing Grafana data and secrets must not be deleted. Restoring a Grafana database is a separate operation, not required for a root URL change.
Remaining operational work
Track MFA deployment, backup restore rehearsals, audit/asset retention and periodic role review as explicit follow-ups. Public reachability and health checks do not substitute for a penetration test or GDPR assessment. Keep actual deployment evidence in restricted storage and update these generic guides whenever behaviour changes.