Commit Graph

6 Commits

Author SHA1 Message Date
Cauê Faleiros
b329f76378 refactor: lay the repository out by role
All checks were successful
Build and deploy / Validate source (push) Successful in 7s
Build and deploy / Integration suite on a real stack (push) Successful in 1m25s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images and notify Portainer (push) Successful in 1m31s
local/ held six unrelated things under a name that stopped being true once it
became the production runtime: the service, the frontend, the tests, the ops
commands, the container definitions and the dependency lock, 65 files with
nothing to tell them apart.

  app/      the service: api/ routers, core/ for identity, database, models,
            prices and secret loading, and the worker, bootstrap and schema
  tests/    the twelve suites, no longer inside the shipped package
  ops/      backup, readiness, dependency audit, security summary
  infra/    Dockerfiles, gateway templates, ClamAV and storage configuration,
            the requirements and their hash lock
  web/      the Site, Kanban and portal pages with their scripts

deploy/Dockerfile.api now copies app/ alone, so the tests stop shipping to
production; the local image still carries them, because the suites run inside
the stack's network.

Five kinds of reference had to follow, and each was found by something different
rather than by reading. Imports of the form "from . import db" survived a rewrite
that only matched "from .db import". Tests kept relative imports of modules that
had left the package. A mock.patch target names its module in a string, where no
import rewriting can see it. The browser test resolves a fixture by path. And the
release gate's markers pointed at local/runtime.py and local/worker.py, which is
the decay its new marker test exists to catch — it caught it.

Verified from docker compose down -v: the stack starts, all six integration
suites, both browser suites and the twenty-nine unit tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 17:40:57 -03:00
Cauê Faleiros
a87403338d feat: give each Kanban operator their own account
All checks were successful
Build and deploy / Validate source (push) Successful in 5s
Build and deploy / Integration suite on a real stack (push) Successful in 1m17s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images and notify Portainer (push) Successful in 1m37s
One OPERATOR_EMAIL and OPERATOR_PASSWORD served the whole factory, so every card
movement recorded the same name and the movement history could not answer who
did what. Traceability was one of the things the project set out to provide.

Accounts live in dtf_local.operators, authenticated with the same scrypt hashing
as customer accounts and with comparable work whether or not the account exists,
so absence is not observable by timing. Administration is a CLI in the API
container, like the schema migration: list, add, password, disable, enable.
Passwords are read from the terminal rather than an argument so they stay out of
shell history and the process list, and disabling deletes that operator's open
sessions instead of leaving them valid for the rest of the eight-hour window.

Migration is the part that could hurt: an empty table means 503 and a factory
locked out of its Kanban. OPERATOR_EMAIL and OPERATOR_PASSWORD seed the first
account, and only when that email is absent, so a password changed through the
CLI survives a redeploy carrying a stale environment variable. The first attempt
at this silently did nothing, because db-init receives its own small environment
and had neither variable; both compose files now pass them to it.

Verified against a running stack: bootstrap seeds the existing credential, that
credential still logs in unchanged, a second operator authenticates separately,
wrong passwords and unknown accounts are rejected alike, and disabling revokes
an open session immediately.

Roles are left out on purpose. The separation of duties the meeting described
governs rework authorisation, which this system does not implement, so a role
model would have no consumer to serve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 14:13:47 -03:00
Cauê Faleiros
24cb52d992 fix: stop the CI stack colliding with ports already used on the runner
The integration job failed with "Bind for 0.0.0.0:8000 failed: port is already
allocated". The runner shares the host's Docker daemon, so every published port
is claimed on the machine itself, where other services already listen. Port 8000
was the first collision; 8080, 8081, 9000 and 9001 were equally exposed.

MinIO's ports were hardcoded, and S3_PUBLIC_ENDPOINT was pinned to
localhost:9000 independently, so moving storage would have broken the presigned
URLs the browser fetches. Both now derive from STORAGE_PORT and move together.

CI runs on 18080/18081/18000/19000/19001. Local defaults are unchanged.

Verified by running the whole stack and the full suite on exactly those ports,
including the browser end-to-end, which downloads through a presigned URL and so
proves the storage endpoint followed the port.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 13:00:17 -03:00
Cauê Faleiros
6a50e6db4d fix: bake scanner and storage config into images instead of bind-mounting
Some checks failed
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Failing after 54s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images and notify Portainer (push) Has been skipped
The integration job failed starting the scanner:

  error mounting ".../local/clamd.conf" to rootfs at "/etc/clamav/clamd.conf":
  not a directory

The files are in the repository, so this was not a missing checkout. A
containerised CI runner shares the host's Docker daemon, so "./local/clamd.conf"
resolves to a workspace path that exists inside the runner but not on the host
where the daemon creates the mount. The daemon makes an empty directory there
and the container cannot start. Only the bind-mounting services were affected,
which is why PostgreSQL and MinIO came up first.

Build the scanner and storage-init images with their configuration copied in, so
compose.local.yaml no longer bind-mounts anything from the host and works
regardless of how the runner reaches the daemon. Both bases stay overridable
through CLAMAV_IMAGE and MINIO_IMAGE.

The production stack is unaffected: it ships clamd.conf as a Swarm config, which
the manager reads at deploy time.

Verified from a clean slate: the stack starts, the scanner runs the baked
configuration, storage provisioning runs from the baked script, and the full
suite passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 12:51:08 -03:00
Cauê Faleiros
6c52ad655e fix: pull MinIO from quay.io so CI can start the stack
The integration job failed on the runner with "pull access denied for
minio/minio ... may require 'docker login'". Docker Hub now refuses anonymous
pulls of minio/minio: an unauthenticated manifest request returns 401
UNAUTHORIZED, while library/postgres returns 200, which is why only MinIO
failed. It worked locally only because this machine is logged in to Docker Hub.

quay.io serves the same release anonymously, and it is the same image: both
registries resolve to image ID sha256:a1ea29fa2835. MINIO_IMAGE overrides it for
anyone mirroring into their own registry.

Verified by deleting the Docker Hub copy locally and starting the stack from
quay alone, then running the full suite against it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 12:33:46 -03:00
Cauê Faleiros
fc6f708e95 chore: restore a working localhost stack
docker-compose.yml became the production/R2 stack, but LOCAL_SETUP.md still
documented "docker compose up --build" against .env.example, which fails on
missing R2_ENDPOINT, SITE_DOMAIN and KANBAN_DOMAIN, and MinIO was gone.

Add compose.local.yaml: builds from source, MinIO storage, fake providers,
disposable credentials, an app database password distinct from the
administrator one, and published origins in ALLOWED_ORIGINS so browser writes
are not rejected. Correct the documented command and the Kanban login, which
listed a username the email-validated model rejects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 18:33:27 -03:00