Aller au contenu

Changelog

All notable changes to this project are documented here.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Each sub-repo (Barrin's API, Barrin's Identity, Tamiyo Scroll, Tolaria News, Docs, Ops) keeps its own CHANGELOG.md — full history, linked in the nav to the left. The section below aggregates just the entries for the most recently released version, across every sub-repo that changed in it, as a quick "what shipped last" summary.

Latest changes — v1.0.0

Barrin's API

Added

  • Initial scaffold of the Barrin's API backend (FastAPI, Python 3.14, PostgreSQL via async SQLAlchemy 2 + asyncpg, Alembic migrations, Pydantic v2 settings).
  • Authentication (/api/v1/auth): JWT (HS256) login/refresh/logout, self-registration with email verification (/auth/signup, /auth/signup/verify, /auth/signup/resend), admin-only direct account creation, Argon2id password hashing, and a 5-level hierarchical role system (anonymoususerrole_cml_developeradmin) enforced via a require_role(min_role) dependency.
  • Tamiyo Scroll BFF (/bff/tamiyo-scroll), a competitive MTG tracking API: user settings (read-only sharing), personal decks and versioned decklists (with Moxfield import), a metagame/opponent deck roster, a BO3 match log, card-test feedback, decklist coloring by test feedback, and aggregated archetype/matchup statistics.
  • Email delivery service with a console sender for local development and an SMTP sender for production.
  • scripts/create_admin.py to bootstrap the first admin account, and scripts/workflow_ci.py to run the full local CI pipeline (ruff, ty, pytest) outside of GitHub Actions.
  • Alembic migrations: users/roles/tokens, auth email verifications, and the Tamiyo Scroll tracker tables.
  • Test suite (pytest + pytest-asyncio + httpx) with a 90% global / 100% app/models/ coverage floor.
  • docs/content/back/barrins_api/auth_roles.md: documents the JWT authentication and hierarchical role-based access control system (role hierarchy, endpoint security matrix), replacing the ad hoc X-Admin-Key header.
  • docs/content/back/barrins_api/signup_email_verification.md: documents the self-registration and email verification flow (/auth/signup, /auth/signup/verify, /auth/signup/resend).
  • Nav entries (docs/mkdocs.yml) and _links.md sidecar links for the two pages above.
  • GET /health: reports database connectivity (200/503), for external uptime monitoring — 503 covers both a failed query and a fully unreachable database (e.g. connection refused/rejected). Registered after GET / in general/router.py so /docs lists routes in that order.
  • External uptime monitoring (HetrixTools) is live against barrins_api prod + staging /health, plus certificate-expiry alerting — see docs/content/ops/operations/index.md.
  • Real Moxfield deck import (app/services/moxfield/): fetches a public deck from Moxfield's API, rate-limited to 1 request/second, replacing the earlier placeholder that only stored the given URL as text.
  • API routes reorganized into one app/api/<domain>/ package per domain (general/, tamiyo_scroll/), each owning its own router aggregator — replaced the previous ts_router.py/bff/ts_router/ and v1_router.py/v1/ split, which had a file and a differently-scoped directory sharing the same name.

Changed

  • Translated README.md from French to English.

Barrin's Identity

Nothing yet.

Tamiyo Scroll

Added

  • Initial scaffold of the Tamiyo Scroll frontend (React 19, TypeScript, Vite, React Router, TanStack Query, Zod, TailwindCSS, shadcn/ui components).
  • Authentication flow: login page, self-registration email verification page, and a ProtectedRoute guard backed by a session store consuming the barrins_api /api/v1/auth endpoints.
  • Metagame tab: personal decks list with Moxfield decklist import, a meta/opponent deck roster, and aggregated archetype/matchup statistics sections.
  • Suivi BO3 tab: match journal, new-match form, and card-test feedback section, backed by the BO3 match log and card-test BFF endpoints.
  • Decklist tab: current decklist view (colored by card-test feedback) and version history section.
  • Read-only "viewing owner" selector (header) and active-deck-context for sharing another user's data without allowing edits, per the BFF's read-only sharing settings.
  • App shell layout with tab navigation, and a centralized typed API client (src/api/client.ts) with Zod-validated request/response schemas (src/schemas/tamiyoScroll.ts).
  • Test suite (Vitest + Testing Library) covering the API client, card tests, active-deck context, match form, and card-tests section.
  • LoginPage.tsx: live password requirements checklist on signup, mirroring PASSWORD_PATTERN in apps/barrins_api/app/schemas/auth.py (12+ characters, upper/lowercase, digit, symbol) so users see which rules they still need to satisfy as they type. UX feedback only — the backend remains the sole source of truth on submit.
  • PersonalDeckSelector.tsx: an archive (delete) button per deck in the list, wired to the already-existing DELETE /personal-decks/{id} endpoint (soft-delete via archived_at, never a hard delete) and useArchivePersonalDeck hook — both existed backend-side but had no UI entry point. Guarded behind a confirmation dialog (ui/dialog.tsx) so a stray click can't archive a deck. Archiving the currently-active deck also clears active_personal_deck_id.
  • AppShell.tsx: a "Welcome, {display_name}" greeting next to the Log out button, using useCurrentUser (GET /api/v1/auth/me, already implemented but unused in the UI). Falls back to the account email when no display name is set.

Changed

  • Route /app/suivi-bo3 renamed to /app/bo3-tracker (App.tsx, AppShell.tsx's tab nav, LoginPage.tsx, VerifyEmailPage.tsx, README.md). The pages/suivi-bo3/ folder and SuiviBo3Tab component name are unaffected — internal naming, not the route.
  • Post-login, post-signup, and post-email-verification now redirect to /app/bo3-tracker instead of /app/metagame, landing users on the match tracker first.
  • Translated README.md from French to English.
  • Translated remaining French UI text (labels, buttons, placeholders, error messages) and code comments across the app — index.css, active-deck-context.tsx, lib/mtg-format.ts, schemas/tamiyoScroll.ts, LoginPage.tsx, VerifyEmailPage.tsx, the decklist, metagame, and Suivi BO3 sections, AppShell.tsx, lib/store.ts, lib/queryClient.ts, api/client.ts, api/viewingOwner.ts, and hooks/useViewingOwner.ts — to English.
  • The "Share my data" checkbox and "View: {user}" selector extracted into SharingControls, disabled for v1.0.0 (not mature enough — the underlying backend enforcement is fully tested, but this UI had no component-level test). AppShell's deck selector no longer branches on canEdit/viewing-mode, since there's no UI path left to enter it.
  • CurrentDecklistSection.tsx, PersonalDecklistImportSection.tsx, and CardTestsSection.tsx now render nothing (null) instead of a "Select or create a personal deck…" placeholder card when no personal deck is active (e.g. right after archiving one) — matches VersionHistorySection.tsx's existing behavior for the same state.

Fixed

  • VerifyEmailPage.tsx: footer credit read "Account managed by barrins_api" — corrected to barrins_identity, the actual identity service.
  • PersonalDecklistImportSection.tsx: handleImport (Moxfield URL) and handleSaveRaw (raw decklist text) never caught a failed mutation, so a backend error (e.g. 400 on an invalid/non-Moxfield URL) silently vanished instead of reaching the user — found via the A3 manual UAT step. Both now catch ApiError and render the message inline, mirroring LoginPage's existing pattern.
  • vite.config.ts: stubbed VITE_API_BASE_URL via Vitest's test.env so src/api/client.ts doesn't build requests against "undefined" during tests. The variable was only ever supplied by a local, gitignored .env file, so every CI run (including the front job for otherwise-unrelated Dependabot bumps) failed 6 client.test.ts tests with TypeError: Invalid URL.

Tolaria News

Nothing yet.

Docs

Added

  • CI-runnable local scripts (docs/package.json): npm run lint, npm run spellcheck, npm run build, and npm run ci, mirroring the docs job in .github/workflows/CI.yml so the same checks can run from a terminal without waiting on CI.
  • docs/cspell.json, a real cspell config the CLI can read (spelling exceptions previously only lived in .vscode/settings.json, which the standalone cspell CLI does not parse).
  • This changelog, following Keep a Changelog and Semantic Versioning.
  • docs/hooks/sync_readmes.py, an on_pre_build mkdocs hook that copies each apps/<app>/README.md into its docs/content/**/<app>/index.md page at build time, so app READMEs and their docs page never drift. A sibling _links.md file per page preserves the curated nav links (e.g. to bff/tamiyo_scroll.md, bootstrap.md, incidents/index.md) that used to live directly in index.md; those sidecars are excluded from the built site via exclude_docs in mkdocs.yml.
  • Root .gitignore: ignores the mkdocs-generated index.md pages, the docs/site/ build output, and Python __pycache__/*.pyc files (produced by the new build hook).
  • docs/hooks/sync_readmes.py extended to also sync each ops/my-server/roles/<role>/README.md into docs/content/ops/roles/<role>/index.md (target directories created and cleaned up on demand, since — unlike the app pages — no _links.md sidecar pre-creates them). New Ops → Roles nav section (docs/mkdocs.yml, placed after Deployment) and docs/content/ops/roles/index.md overview page link the eight generated role pages; .gitignore and the generated-marker comment in the hook itself were generalized to cover both README sources.
  • on_shutdown hook in docs/hooks/sync_readmes.py: deletes the generated index.md pages when the hook shuts down, since mkdocs serve keeps rewriting them on every reload but never removes them on its own.
  • docs/hooks/sync_changelogs.py: a second on_pre_build hook, mirroring sync_readmes.py's pattern, that copies each sub-repo's own CHANGELOG.md into docs/content/changelog/<subrepo>.md and builds docs/content/changelog/index.md (a tracked _intro.md partial plus a "Latest changes" section aggregated from whichever [X.Y.Z] section matches the latest vX.Y.Z git tag). Replaces the single hand-maintained docs/content/CHANGELOG.md this entry itself used to live in.

Changed

  • docs/package.json: merged spellcheck and spellcheck-app into a single spellcheck script covering both content/**/*.md and ../apps/**/*.md.
  • .github/workflows/CI.yml: excluded _links.md sidecar files from the markdownlint and cspell steps (they intentionally start with a bullet list rather than a heading).
  • docs/cspell.json: added the technical terms and proper nouns introduced while translating app READMEs and wiring the README sync hook (asyncpg, cffi, decklist, getpass, metagame, Moxfield, MTGJSON, mypy, oxlint, pytest, Resends, venv, winrate/winrates, among others).
  • docs/cspell.json: removed the blanket *.yml ignore in favor of a !docs/**/*.{yml,yaml} exception (so files like docs/mkdocs.yml are spell-checked) and added **/*.toml to ignorePaths; added terms surfaced by the new auth/signup documentation (checkfirst, passlib, pyproject, Referer, STARTTLS, userrole, VARCHAR, among others).
  • docs/cspell.json: added subdir, introduced by the new *_repo_subdir Ansible role variable documented below (ops section).

Fixed

  • mkdocs.yml had docs_dir: content pointing at a folder that did not exist; all documentation pages moved under docs/content/ to match it.
  • mkdocs.yml nav referenced back/barrins_api/implementation.md, which does not exist (the actual page is back/barrins_api/bff/tamiyo_scroll.md); also added the missing nav entries for front/tamiyo_scroll/bootstrap.md and the incidents pages, which were causing mkdocs build --strict to fail.

Ops

Added

  • .github/workflows/CI.yml: path-filtered CI pipeline (dorny/paths-filter) that runs checks only for the parts of the monorepo a change actually touches — back (lint, security, types, tests via uv run scripts/workflow_ci.py for barrins_api/ barrins_identity), front (npm run lint/build/test for tamiyo_scroll/tolaria_news), ops (ansible-lint for ops/my-server), and docs (markdownlint, cspell, mkdocs build --strict) — on every push/PR to staging and main. A ci-required job aggregates the per-job results into a single fail-closed status check, so branch protection has one check to depend on even though the individual jobs are conditionally skipped.
  • .github/dependabot.yml: weekly dependency update PRs for apps/barrins_api (uv), apps/tamiyo_scroll (npm), docs (npm), and .github/workflows (github-actions), all targeting staging so updates go through the same CI gate as any other change before reaching main. Dependabot only ever opens pull requests — it never pushes commits directly to a branch.
  • .github/workflows/deploy-docs.yml: manual (workflow_dispatch) MkDocs build-and-deploy workflow to GitHub Pages, intentionally kept out of the required CI checks. The hosting target (GitHub Pages + custom domain docs.barrins-codex.org) is a placeholder pending confirmation. Superseded: this workflow was never actually committed to the repo, and the hosting target is now decided — self-hosted via docs.yml/the docs_site role (see Added, below), not GitHub Pages (the account already hosts other, unrelated projects' Pages sites — see B2, docs/project/first_production_release/b2-docs-deploy/index.md).
  • Ansible VPS deployment (ops/my-server/), moved in-repo from the previous separate Spigushe/myserver repository (now deprecated) so infrastructure changes land alongside the application changes that require them (Constitution §26.1). Playbooks: initial.yml, setup.yml, barrins_api.yml, tamiyo_scroll.yml, tolaria_news.yml; roles: create_ssh_key, setup_base_user, setup_packages, register_ssl, backend_website, react_frontend, fastapi_backend. scripts/check_no_secrets_committed.sh guards against ever staging a real secrets file. Documented under the new Constitution §38-mandated docs/content/ops/ tree (architecture/independence.md, architecture/decisions.md for the ADRs below, deployment/{backend,frontend,rollback}.md, security/secrets.md, operations/index.md — the last honestly documenting current gaps: no health endpoint, no monitoring, no tested backups).
  • ops/my-server/postgresql_pgadmin.yml and the pgadmin role: a Docker-based pgAdmin4 deployment/administration playbook (isolated Docker network, pg_hba.conf/listen_addresses wiring, weekly auto-update timer, unattended-upgrades for the host), porting the pgadmin role from myserver's unmerged postgresql-pgadmin-playbook branch. PostgreSQL itself is already installed by setup_packages at host bootstrap; this playbook only exposes it via pgAdmin. Documented at docs/content/ops/deployment/database.md.
  • ops/my-server/secrets/tamiyo_scroll/{staging,production}.env.example: documentation templates mirroring apps/tamiyo_scroll/.env.example, for parity with barrins_api's per-app secrets/ layout. Unlike fastapi_backend_env_file, react_frontend has no env_file mechanism — tamiyo_scroll.yml does not read these; VITE_API_BASE_URL is already computed automatically by the playbook and isn't secret.
  • ops/my-server/roles/postgres_backup, wired into postgresql_pgadmin.yml: a systemd-timer-driven daily backup (pg_dump --format=custom per non-template database, plus pg_dumpall --globals-only for roles/grants) into /var/backups/postgresql/, 14-day retention, running as the postgres OS user (umask 077 makes every dump 0600 by construction). Documented at docs/content/ops/deployment/backup.md. UAT-verified on staging: timer active, a manually-triggered run produced dumps with the correct permissions (0600, owned by postgres) for every database on the instance, and a full restore drill (restore into a scratch database, SELECT count(*) compared against the live database) matched exactly.
  • ops/my-server/secrets/postgresql_pgadmin/admin_password.txt.example, ops/my-server/secrets/github/token.txt.example: plaintext templates for two secrets secrets/README.md already documented but never actually shipped — a bare pgAdmin admin password file, and a shared GitHub PAT (see Changed, below).
  • ops/my-server/roles/github_token: the GitHub PAT pre_tasks block (see Changed, below) extracted into its own role once docs.yml became its fourth identical copy across barrins_api.yml, tamiyo_scroll.yml, and tolaria_news.yml. Every playbook that clones a private repo now lists github_token first in its roles:; fastapi_backend/react_frontend/docs_site fall back to its github_token fact via <role>_github_token | default(github_token) unless a playbook overrides it per-invocation.
  • ops/my-server/docs.yml and the docs_site role: self-hosted deployment of the mkdocs documentation site (B2), same deploy_env/branch/release-tag options as barrins_api.yml/ tamiyo_scroll.yml. Structurally mirrors react_frontend (clone at a ref, build, symlink static output, hand ownership to www-data, nginx vhost + register_ssl), but builds with uv/uvx (uvx --with mkdocs-material mkdocs build --strict, matching fastapi_backend's toolchain) instead of nvm/npm, and serves a plain static multi-page site (try_files $uri $uri/ =404;) rather than a client-side-routed SPA. Decided over GitHub Pages because the account already hosts other, unrelated projects' Pages sites. Documented at docs/content/ops/deployment/docs_site.md.

Changed

  • .github/workflows/CI.yml: translated remaining French inline comments and step names to English.
  • Constitution §34 (Secrets Management), applied while moving ops/my-server/ in-repo and decided with the user rather than guessed (§16.2, recorded as an ADR in docs/content/ops/architecture/decisions.md): backend .env files are local-only and git-ignored (ops/my-server/secrets/**/*.env), never committed even encrypted. fastapi_backend's fastapi_backend_env_file step uses one if present on the operator's machine, skips gracefully otherwise.
  • Constitution §25/§27/§31 (Release Policy), same ADR process: production deploys resolve the latest GitHub release tag by default (fastapi_backend_use_release_tag/react_frontend_use_release_tag, wired to deploy_env == 'production' in every playbook), or a pinned tag for rollback. Staging keeps deploying a branch, since it exists to preview code before release.
  • .gitignore and scripts/check_no_secrets_committed.sh generalized: allow-list secrets/**/*.example and README.md instead of listing each secret filename individually, so a new secret file (e.g. pgAdmin's admin password) is caught by default without a new gitignore line.
  • Constitution §26.1 (Infrastructure objective): added an explicit "one application, one playbook" rule — a frontend playbook must never embed a backend role invocation (or vice versa), and running one app's playbook must never touch another's systemd service, nginx vhost, or database. Decided with the user (§16.2) after tolaria_news.yml's embedded-backend exception (see Fixed below) was judged to need fixing rather than being grandfathered.
  • ops/my-server/ansible.cfg: normalized key=value spacing under [defaults]inventory, vault_password_file, timeout, and ansible_ssh_user had stray spaces around = that every other key in the file didn't.
  • Production email sending (ADR-3): decided to go through a transactional email provider's SMTP relay for identity@barrins-codex.org, rather than self-hosting a mail server or keeping the temporary personal Gmail relay long-term. No code change required — SMTPEmailSender already speaks generic SMTP.
  • ops/my-server/barrins_api.yml, tamiyo_scroll.yml, tolaria_news.yml, docs.yml: the shared GitHub PAT is no longer an inline ansible-vault-encrypted blob duplicated across the playbooks — externalized to a single local, git-ignored secrets/github/token.txt (same pattern as postgresql_pgadmin's admin password), read via the github_token role (see Added, above) — originally a fail-fast pre_tasks block copy-pasted into each playbook, extracted into a role once docs.yml made it a fourth copy. Renewing the token (every 30 days) is now overwriting one local file, not re-encrypting and editing three tracked files.

Fixed

  • .github/workflows/CI.yml: the back job never provisioned a Postgres service or a real SECRET_KEY, so any PR touching apps/barrins_api was doomed to fail — pytest errors out while tests/conftest.py imports app.config.settings (the placeholder SECRET_KEY is rejected), and even past that the session-scoped test_engine fixture needs a reachable database. This went unnoticed because no PR had touched apps/barrins_api since the CI pipeline was wired up. Added a postgres:17 service container (localhost:5432, health-checked via pg_isready), job-level DATABASE_URL/TEST_DATABASE_URL env vars pointing at it, and a step generating an ephemeral SECRET_KEY via openssl rand -hex 32 before workflow_ci.py runs.
  • ops/my-server playbooks/roles/READMEs: become: 'no'/'yes' (and gather_facts/update/force/recurse/daemon_reload/enabled/ update_cache using the same quoted-string pattern) are YAML strings, not booleans — schema/lint tools correctly flagged them as "Incorrect type. Expected boolean." Replaced every instance with real true/false.
  • .github/workflows/CI.yml: the ops job ran ansible-lint ops/myserver — the original scaffold's placeholder path (marked # TO UPDATE) that nobody updated once the real Ansible deployment landed at ops/my-server/ — so the job failed outright (ops/myserver: File or directory not found). Corrected the path. The new docs/content/ops/** tree also never passed the docs job's actual checks: cspell didn't know fastapi, pgadmin, certbot, journalctl, frontends, spigushe, uvicorn, HSTS, vhosts, dpage, certonly, creatordate, nohostname, inlines, ciphertext, FQCN, or keypair (added to docs/cspell.json), and deployment/backend.md had a code-block line over the 80-char MD013 limit.
  • ops/my-server: fixing the ansible-lint path above revealed the playbooks/roles themselves failed the same job hard — 114 failures + 33 warnings across 38 files, none of it caught before since this was the first time ansible-lint actually reached ops/my-server (see the path bug above). Brought it to a clean pass at ansible-lint's production profile (ops CI only requires min):
  • fqcn (73×): every builtin module action FQCN-prefixed (ansible.builtin.copy, not copy).
  • syntax-check[unknown-module] (2×): openssh_keypair and authorized_key moved out of ansible.builtin — switched to community.crypto.openssh_keypair/ansible.posix.authorized_key and added ops/my-server/requirements.yml declaring both collections (ansible-galaxy collection install -r requirements.yml, also wired into the ops CI job before linting).
  • role-name (7×): renamed every hyphenated role directory to snake_case (fastapi-backendfastapi_backend, react-frontendreact_frontend, backend-websitebackend_website, create-ssh-keycreate_ssh_key, register-sslregister_ssl, setup-base-usersetup_base_user, setup-packagessetup_packages) and updated every reference across playbooks and docs.
  • var-naming[no-role-prefix] (56× once the roles above had valid names — the rule doesn't check unnamed roles): every role-input var and internal computed-config dict renamed to carry the full role name as prefix — abbreviations like fb_repo became fastapi_backend_repo; the bare single-letter config dicts (r, and pgadmin's own r) became <role>_config (fastapi_backend_config, pgadmin_config, etc.).
  • risky-file-permissions (8×): explicit mode: added to every template/copy task that lacked one.
  • yaml[octal-values] (4×): mode: 0755/0600 quoted ("0755"/"0600").
  • no-changed-when (4×): explicit changed_when added to command/shell tasks that always reported "changed".
  • package-latest (1×): the pip fallback install now pins state: present instead of latest, consistent with installing from a requirements.txt in the first place.
  • name (9×): missing play/task names added; the one task name embedding a Jinja expression mid-string moved it to the end.
  • jinja[spacing] (33× warnings): {{var}}{{ var }} throughout, including .j2 templates (not part of the lint gate, fixed for consistency while touching the surrounding code). Verified with ansible-playbook --syntax-check on all six top-level playbooks (ansible-lint itself needs the POSIX grp module and doesn't run natively on Windows — validated from a WSL/Linux venv). New Constitution subsection (docs/content/CLAUDE.md §26.4, "Ansible coding standards") distills these rules for future playbook/role work.
  • ops/my-server/roles/fastapi_backend, react_frontend: fastapi_backend_repo/react_frontend_repo in barrins_api.yml, tamiyo_scroll.yml, tolaria_news.yml pointed at barrins-project/barrins_api, barrins-project/tamiyo_scroll, barrins-project/tolaria_news — repos that don't exist; the apps actually live under apps/<name>/ in this monorepo (Spigushe/barrins-project). The first deploy would have failed at the git clone step. Both roles gained a *_repo_subdir var: the full repo is still cloned to app_root/site_root, but dependency detection/install, the deployed .env, the build command, and the systemd WorkingDirectory now resolve against <root>/<repo_subdir>; the three playbooks were repointed at Spigushe/barrins-project with the matching apps/<name> subdir. Also surfaced and fixed a related latent bug: react_frontend's dist symlink task only fired when build_dir != 'dist', which would have silently served nothing once a subdir is introduced (build output lands at <site_root>/<subdir>/dist, not <site_root>/dist) — the condition now compares full resolved paths instead.
  • ops/my-server/roles/register_ssl/tasks/main.yml: the role's own README documents templating /etc/nginx/snippets/ssl-params.conf as step 1 — shared by every HTTPS-serving role via include snippets/ssl-params.conf; (backend_website, react_frontend, pgadmin) — but the task that actually templates it was dropped when the Ansible deployment moved in-repo; tasks/main.yml went straight from the HTTP vhost to reload to certbot. Any HTTPS vhost reload on a host missing that snippet failed nginx's config test outright (open() "/etc/nginx/snippets/ssl-params.conf" failed (2: No such file or directory)), surfaced when deploying barrins_api.yml -e deploy_env=staging to a fresh domain. Restored the missing task, ordered first as the README already described.
  • Two WSL-specific gotchas surfaced while diagnosing the above from a /mnt/c (DrvFs) checkout, not repo bugs but worth recording for any operator deploying from WSL: DrvFs mounts report their directories as world-writable by default, so Ansible silently ignores a same-directory ansible.cfg (inventory/ansible_ssh_user never applied, inventory host pattern left unmatched); and DrvFs's default file permissions can leave the owner-execute bit set even after narrowing fmask, which makes Ansible mistake .vault-password-file.txt for a vault password script rather than a plain password file. Both resolved by setting metadata,umask=22,fmask=111 under [automount] in /etc/wsl.conf (fmask=111 clears execute for owner/group/other alike, vs. a narrower fmask=11 which left owner's execute bit set) followed by wsl --shutdown.
  • ops/my-server/tolaria_news.yml: dropped the embedded copy of the barrins_api backend role block (previously documented as a known exception to "one playbook per app" — see Constitution §26.1 above). It's frontend-only now, pointing VITE_API_BASE_URL at whatever barrins_api.yml already has running, the same pattern tamiyo_scroll.yml already used. Updated the SSH/Alembic path reminders (~/projects/<domain>/apps/barrins_api, not the app root) and every doc referencing the stray tolaria.yml filename (the file has always been tolaria_news.yml): README.md, architecture/independence.md, deployment/frontend.md, deployment/rollback.md.
  • ops/my-server/roles/fastapi_backend/tasks/main.yml: no task ever ran alembic upgrade head — the role installed dependencies, deployed .env, and restarted the service, leaving any pending schema migration unapplied against the newly deployed code (Constitution §31.1/§37.1 both list migrations as a required deployment step). Added an "Apply database migrations" task (uv run alembic upgrade head, chdir at the resolved work dir), gated on the same fastapi_backend_pyproject.stat.exists check as the uv sync task.
  • ops/my-server/roles/fastapi_backend/tasks/main.yml: the migrations task above initially ran right after dependency installation, before the .env deploy step — so alembic upgrade head connected using whatever .env (if any) was already sitting on the server from a previous deploy, not the one this run just copied. Surfaced while deploying barrins_api.yml -e deploy_env=staging: migrations failed with password authentication failed for user "REPLACE_USER" even after the local secrets/barrins_api/staging.env was fixed, because the corrected file hadn't been copied to the server yet at the point migrations ran. Reordered so "Deploy the local .env file" (and its "no local .env found" fallback) runs before "Apply database migrations".
  • ops/my-server/barrins_api.yml, tamiyo_scroll.yml, tolaria_news.yml: env_branch defaulted staging deploys to a develop branch that doesn't exist in this repo (the actual branch is staging) — any -e deploy_env=staging run would have failed at the git clone/checkout step. Also populated the three playbooks' github_token vault block, still a REPLACE_WITH_... placeholder ciphertext since the ops migration in-repo, with the real ansible-vault encrypt_string output.
  • ops/my-server/README.md: the venv setup instructions created a bare venv/ directory, which nothing but a stale, since-superseded .gitignore line covers — every role actually uses uv's .venv convention (caught by the repo-wide **/.venv/ pattern). Renamed to .venv, and added --force to the ansible-galaxy collection install step so it actually reinstalls collections pinned in requirements.yml instead of silently skipping already-installed ones.
  • ops/my-server/roles/fastapi_backend/README.md: documented the uv python install 3.14 step the role already performs before uv sync (idempotent, a no-op when already installed) — the README previously only mentioned uv sync itself.
  • ops/my-server/roles/react_frontend/tasks/main.yml: the "Ensure nvm is installed" task set NVM_DIR=/opt/nvm (shared across apps) but never created that directory first; nvm's install.sh only auto-creates $NVM_DIR when it matches its own default ($HOME/.nvm), so it exited with "You have $NVM_DIR set... but that directory does not exist" on a fresh host. Surfaced while deploying tamiyo_scroll.yml -e deploy_env=staging to a fresh server. Added a task creating /opt/nvm before the install step.
  • ops/my-server/roles/react_frontend/tasks/main.yml: the "Clone/update application repository" task runs as root (the play's become: true default), but the role's last task hands site_root to www-data recursively — including the .git/ directory — so on the next run root no longer owns the repo and git refuses it outright (detected dubious ownership in repository at ..., Git's CVE-2022-24765 protection). Surfaced on a redeploy of tamiyo_scroll.yml -e deploy_env=staging after the initial deploy had already flipped ownership to www-data. Added a task setting safe.directory in root's global gitconfig (as root) before the clone/update step, so every future run is immune to the ownership check regardless of who last owned the checkout. First written as an ansible.builtin.command: git config --global --add safe.directory ... task, which passed locally but failed PR #17's ops CI job under ansible-lint's command-instead-of-module rule; switched to community.general.git_config (add_mode: add, which maps to git config --add and is idempotent — it skips when the value is already present), adding community.general to ops/my-server/requirements.yml.
  • ops/my-server/roles/react_frontend/tasks/main.yml: with the ownership check above resolved, the same "Clone/update application repository" task then failed with Local modifications exist in the destination ... (force=no) on a second deploy — the build step (npm install/ build command) can leave the working tree dirty (e.g. a regenerated lockfile), and Ansible's git module refuses to update over local modifications unless told to. Since this checkout exists solely to be rebuilt from source control on every deploy, added force: true so it always resets cleanly to the target ref. Verified with a full tamiyo_scroll.yml -e deploy_env=staging redeploy, which completed successfully end to end.
  • .gitignore: the secrets/** allow-list only worked for secrets/barrins_api/ because that directory predates the rule — for any brand-new subdirectory under secrets/ (surfaced while adding secrets/postgresql_pgadmin/ and secrets/github/), git excluded the whole directory before evaluating the *.example negation, a parent-directory-excluded gitignore gotcha. Added !ops/my-server/secrets/*/ so new secret subdirectories are traversed correctly regardless of what they're named.
  • ops/my-server/roles/register_ssl/vars/main.yml: register_ssl_contact_name defaulted to <username>@<domain> — a mailbox that never existed on any of the 4 domains this role certifies. Let's Encrypt doesn't verify deliverability at registration time, so certbot never complained; every certificate issued so far was registered with a bogus contact. Made the var required (no default, matching register_ssl_server_name) and passed a real, monitored address from every playbook invoking the role.
  • ops/my-server/postgresql_pgadmin.yml: both pgadmin_admin_email: "{{ pgadmin_admin_email }}" and pgadmin_admin_password: "{{ pgadmin_admin_password }}" passed a role parameter whose value referenced a play-level var of the exact same name — Ansible resolved the right-hand side against the role parameter being defined, an infinite self-reference ("Recursive loop detected in template"). Renamed the play-level vars (admin_contact_email, pgadmin_admin_password_value) so they no longer collide with the pgadmin role's own input var names.
  • ops/my-server/roles/pgadmin: the role unconditionally overwrote postgresql.conf's listen_addresses to "localhost,<gateway>" — on a host where it was already '*' (needed by barrins_api itself to reach Postgres over the public interface, per its DATABASE_URL), this produced a duplicate directive and broke that connectivity, since Postgres uses whichever line appears last in the file. Surfaced live while re-testing barrins_api.yml right after postgresql_pgadmin.yml's first run. The task now reads the active value first and only narrows it when nothing already covers the gateway IP. Also dropped --add-host=host.docker.internal:host-gateway from the pgAdmin container — unnecessary (it already reaches Postgres directly via the isolated network's gateway) and actively misleading, since on Linux host-gateway resolves to the default bridge's gateway, not this custom network's.
  • ops/my-server/roles/docs_site/tasks/main.yml: the "Build the docs site" task carried a become: false copy-pasted from fastapi_backend's uv-install pattern — but unlike fastapi_backend (which clones and builds entirely as the unprivileged user), docs_site clones and builds as root like react_frontend, only handing ownership to www-data at the very end. Building as the unprivileged user against a still root-owned checkout failed outright: PermissionError writing docs/content/back/barrins_api/index.md, regenerated by docs/hooks/sync_readmes.py's mkdocs pre-build hook. Surfaced deploying docs.yml -e deploy_env=staging. Dropped the stray become: false so the build step runs as root, consistent with react_frontend's equivalent build task.
  • ops/my-server/roles/github_token/tasks/main.yml: the ops CI job failed on var-naming[no-role-prefix]ansible-lint wants every fact a role sets prefixed with the role's own name, but this role's entire purpose is to set a shared, unprefixed github_token fact that fastapi_backend/react_frontend/docs_site (and every playbook invoking them) already fall back to by that exact bare name. Prefixing it would break that contract. Suppressed with a targeted # noqa: var-naming[no-role-prefix] and a comment explaining why, rather than renaming.
  • .github/workflows/CI.yml: the changes job's outputs: map exposed back/front/ops but not docs, even though the same job's paths-filter step already computed it — needs.changes.outputs.docs was therefore always empty, so the docs job (if: needs.changes.outputs.docs == 'true') was skipped on every PR regardless of what actually changed, not just this one. Surfaced by a PR touching only docs/project/** still showing ci / docs skipped. Added the missing docs: ${{ steps.filter.outputs.docs }} line.