docs: move the engineering documents into docs/
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 1m18s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images and notify Portainer (push) Successful in 1m26s

Thirteen files at the repository root, seven of them documents. Only README.md
earns a place there; the rest are now in docs/ beside the meeting notes, the
client roadmap and the historical material.

The compose files stay. docker-compose.yml is the path the dtf-cloud Portainer
stack reads, so moving it would break deployment, and Docker resolves a compose
file's relative build contexts against its own directory, so moving the other
two would silently break every build. Both reasons are now written down where
someone would otherwise try it.

Correcting references turned up a live fault: the Portainer stack creation
instructions still named deploy/stack.yaml as the compose path. That file was
removed, so anyone recreating the stack from these instructions would have
failed. It names docker-compose.yml now, with the reason it stays at the root.

ROADMAP.md keeps the paths its closed findings were written with, and says so at
the top. Those entries record where a fault was when it was found; rewriting
them to match a later layout would make the record less true, not more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-21 17:52:20 -03:00
parent b329f76378
commit 7386469404
11 changed files with 26 additions and 21 deletions

403
docs/CONTEXT.md Normal file
View File

@@ -0,0 +1,403 @@
# DTF System - Project Context for AI Agents
## Purpose of this document
Read this document before changing code, documentation, architecture, or scope.
It is the current English source of truth for the project. It supersedes older
decisions in `README.md`, `docs/historico/ESPECIFICACAO.md`, `schema.sql`, and the existing
prototype code whenever they conflict.
Update this file whenever the team makes a material product, process, or
architecture decision.
## Project goal
Build a DTF ordering and production-flow system for Dropstar/Altus.
The current process is slow and manual: customers send artwork through
WhatsApp, staff forward files, designers discover problems late, and production
status is not visible outside the factory. The business has substantially more
machine capacity than it currently uses. The goal is to let customers place DTF
orders online, pay, send artwork, and follow production without making
WhatsApp, shared folders, or manual handoffs the bottleneck.
The customer-facing value is speed and clarity. The operations value is a
traceable queue and fewer lost or manually handled orders.
## Current MVP scope
The MVP must be delivered in **at most three weeks**. The target is a working
online system, not factory-machine automation.
### Included
- Dedicated DTF site/subdomain.
- Existing Site DTF commercial rules and product experience.
- Direct, resumable multipart file upload to Cloudflare R2.
- Mercado Pago payment integration with signed and idempotent webhooks.
- Freight quotation and charging at checkout.
- Idempotent Tiny/Olist order integration and order traceability.
- Online Kanban for production status and secure file download.
- WhatsApp status notifications.
- Private object storage, 30-day file retention, backups, and basic security.
- Operator guidance for the factory team.
### Explicitly outside the three-week delivery
- Automatic pre-flight validation. It will be validated after delivery using
real customer files and real printed output, then refined as support work.
- Direct FlexiPRINT integration.
- Factory-side agent, hot folder, internal file server automation, VPN, and
heartbeat monitoring.
- Production-room dashboard, printer/machine telemetry, and advanced reports.
- Automated shipping-label purchase, shipping-label printing, and dispatch
automation. The MVP freight scope is quote selection and charging only.
- Personalized cart behaviour based on past orders, day, or time.
After delivery, the team provides support and evaluates requested changes. Do
not turn post-delivery support into a new committed delivery phase without an
explicit decision.
## Agreed production model
```text
Customer browser
-> Site DTF / API on VPS
-> direct multipart upload to private Cloudflare R2
-> Mercado Pago payment + freight selected at checkout
-> Tiny/Olist order record
-> online Kanban
-> factory operator downloads final file and imports it manually into FlexiPRINT
```
WhatsApp is a notification channel, not the artwork-upload channel. It is used
for payment confirmation, correction requests, production status, and order
completion.
The operator works through the browser. The factory does not need an installed
agent or an internal server for the MVP.
If the factory internet connection fails, only the factory team temporarily
loses access to the Kanban and secure downloads. The public portal, payments,
uploads, R2 objects, queue, and cloud services remain online. Factory work
resumes when local access returns.
## Infrastructure and deployment
- **VPS:** runs the website, API, worker, PostgreSQL, ClamAV, Kanban, and
third-party integrations.
- **Cloudflare R2:** private S3-compatible object storage for original uploads
and final files. It does not run the application, database, worker, or
antivirus.
- **Upload path:** the browser uploads directly to R2 using short-lived,
server-issued presigned multipart URLs. Large files must never be proxied
through the VPS.
- **Deployment:** one Portainer-owned `dtf-cloud` Docker Swarm stack. One Gitea
Actions workflow tests/scans, publishes `latest` plus commit-SHA application
images, and calls the Portainer webhook. Activation remains blocked pending
the production work listed below.
- **Kanban access:** configure `OPERATOR_EMAIL` and `OPERATOR_PASSWORD` as
stack environment variables. If the email is absent, only Kanban login is
blocked; the rest of the stack remains available.
- **Recommended VPS baseline:** 4 vCPU, 16 GB RAM, and 200 GB NVMe. Existing
VPS capacity may be used if it safely meets or exceeds this baseline.
- **Backups:** PostgreSQL backups go to R2. Application secrets are never
committed to Git.
## File retention
The business does not want an unlimited artwork library.
| Artifact | Retention |
|---|---:|
| Incomplete multipart upload | 1 day |
| Rejected or infected upload | 3 days |
| Customer original after approval | Up to 7 days |
| Final print artwork | Maximum 30 days from the first upload |
| Order history and metrics | Kept in the database without keeping the image |
The 30-day retention decision overrides older references to 90 days or
12-month artwork reordering.
## R2 planning cost reference
R2 cost is driven primarily by the average number of gigabytes stored during a
month; read/write operations are expected to be a much smaller cost at the
project's scale. There is no egress charge in the current planning model.
The client-facing roadmap uses the following planning examples, based on
standard R2 storage pricing, the included storage allowance, and an exchange
rate reference of USD 1 = BRL 5.13:
| Average storage | Approx. R2/month | Approx. BRL/month |
|---|---:|---:|
| 100 GB | USD 1.35 | BRL 7 |
| 500 GB | USD 7.35 | BRL 38 |
| 1 TB | USD 14.85 | BRL 76 |
| 3 TB | USD 44.85 | BRL 230 |
These are planning estimates only. Confirm Cloudflare pricing, exchange rates,
taxes, and payment-provider fees before presenting a commercial quote.
## Commercial rules
The existing rules in `web/index.html` are approved as the current source of
truth. Do **not** redesign, simplify, or change prices, discounts, minimums,
rounding, or product modes without explicit approval.
The current site contains four product modes, quality-based price tiers,
artwork-ready discounts, separate assembly charges for loose artwork, a
one-metre minimum, and ten-centimetre billing rounding.
The browser may display the calculation, but production payment creation must
recalculate and validate all price, quantity, freight, and discount values on
the server. Client-provided totals are never authoritative.
Normal JPG/PNG uploads are individual artwork by default, including when uploaded
from the by-metre panel. They use the existing loose-artwork editor, printed
width, quantity, and live packing canvas; packed height determines metres.
Customers uploading an already assembled film-width image must explicitly select
the ready-sheet option before uploading. PDF and manual-format ready sheets keep
their existing workflow. These models retain their separate approved price tiers;
image dimensions alone must never classify a normal artwork as a finished sheet.
## Freight
The original visual freight flow in `web/index.html` was a stub with only pickup
functional. The localhost bridge now supports pickup and backend fake freight
quotes; there is still no real carrier quotation or production checkout.
The customer already uses Correios and other shipping platforms. Before freight
can be completed, obtain:
- The platform(s) that should be used as the source of truth.
- API credentials or delegated access for the selected platform.
- Origin postal code/address.
- Available services and carriers to offer.
- Packaging weight and dimensions by DTF length/package.
- Freight business policy: exact pass-through, subsidy, free-shipping rules,
pickup, or other exceptions.
At checkout, the backend must quote freight from the selected source, store the
chosen service and quoted amount, include the amount in the Mercado Pago
payment, and persist it in the order. Do not create a payment before the freight
amount is final.
## Integrations
### Mercado Pago
Required before production payment integration, not for the local mock milestone:
- Production credentials.
- Webhook configuration/access.
Webhook processing must verify authenticity and be idempotent. A duplicate or
late delivery must not create a duplicate payment, order, message, or queue
card.
### Tiny/Olist
Tiny/Olist is already available. The implementation must create/update orders
idempotently and use the order number for traceability. Confirm the actual API
endpoints, marker/tag behaviour, and rate limits before production use.
### WhatsApp
WhatsApp is already available, but the technical provider still needs to be
confirmed (official Meta Cloud API, Z-API, or another provider).
Implement only the required customer events:
1. Payment approved.
2. Artwork correction needed, with a secure link back to the site.
3. Order entered production.
4. Order ready for collection or shipping.
Messages initiated by the business may require approved templates depending on
the provider and conversation state. Use an outbox/job mechanism with retries,
delivery status handling, and idempotency. Do not send artwork through
WhatsApp.
## Factory responsibilities after delivery
The client/factory team is responsible for:
- Opening the Kanban, downloading final files, and importing them manually into
the current FlexiPRINT setup.
- Performing real print tests and reporting mismatches in the final file or
printed result.
- Maintaining PCs, printers, FlexiPRINT licences, printing profiles, internal
folders, and the local factory network.
- Informing the development team about changes to prices, freight, operational
rules, or external platforms that need system changes.
The development team delivers the online system, provides usage guidance, and
supports subsequent corrections or scoped enhancements.
## Three-week roadmap
| Week | Delivery | Result |
|---|---|---|
| 1 | Infrastructure and upload | VPS, domain, database, R2, multipart upload, antivirus, and service foundation. |
| 2 | Payment, freight, and order | Mercado Pago, freight quotation, idempotent Tiny/Olist order creation, final file generation, and main Kanban states. |
| 3 | Kanban and handover | Online Kanban, secure download, WhatsApp status messages, 30-day retention, and operational handover. |
## Known implementation status
### Active localhost milestone (2026-09-15)
The current implementation target is localhost before any production connection.
Use `docker-compose.yml`, `.env.example`, and `LOCAL_SETUP.md`. The runtime is in
`app/`; historical `portal/`, `kanban/`, `agente/`, and `schema.sql` are preserved
as references and are not imported or started by Compose.
- One local `dtf-cloud` Compose project runs seven long-lived services: Site and
Kanban web gateways, FastAPI Portal/API, PostgreSQL, MinIO, ClamAV, and a
PostgreSQL outbox/scanning worker. Separate database and storage initialization
jobs provision restricted runtime identities, for nine Compose services total.
- MinIO implements the S3 storage boundary with direct multipart browser uploads,
resumable parts, private objects, and short-lived signed operator downloads.
- Payment, freight, Tiny/Olist, and WhatsApp use fake adapters only. No production
credentials or integrations are configured. The API and worker have no external
network route; only local web/storage gateways publish loopback ports.
- The original Site commercial calculation and appearance remain the source of
truth. Its existing browser artwork analysis is retained as prototype advice;
no new automatic pre-flight, server artwork analysis, or print-file generation
is implemented or invoked.
- Since client length and grade could be tampered with and automatic pre-flight
is deferred, a local operator manually confirms those commercial inputs before
checkout. This is a development trust-boundary decision, not a new production
promise. The backend calculates all tiers, discounts, assembly-inclusive rates,
one-metre minimum, ten-centimetre rounding, freight, and final totals. Immutable
approved quotes expire after 24 hours. A customer confirms the server total
on the Site to create one idempotent local paid order.
- Paid orders use `rec`, `tra`, `fil`, `imp`, `cor`, `fin`, with allowed transitions,
operator authentication, concurrency checks, and durable movement history.
Local mock integration receipts are visible in the Kanban.
- The customer portal at `/portal.html` supports local registration/login,
stronger scrypt password hashing with legacy-hash upgrade, revocable HttpOnly
sessions, owned order history/status, secure active
file downloads, and correction uploads. Registration/login can claim only the
current guest session's records, never orders found by email or CNPJ. Email
verification and password recovery are not implemented.
- Unfinished carts (including File blobs) recover from IndexedDB for 24 hours in
the same browser, scoped to the guest/account identity. Recovered items can be
removed/replaced or joined by new items; in-place reconstruction of the artwork
editor and cross-device cart synchronization are not implemented. Browser
storage capacity limits apply. Payment clears the saved cart. Logout revokes
the server session and clears local checkout metadata and saved File blobs
across open Site tabs.
- Operators manually upload and approve a complete final-file set, with one or
more files per order item. Queue entry requires active final files for every
item. Corrections invalidate the old set; customers submit corrections through
their portal, and operators reapprove final files. No artwork transformation,
automatic pre-flight, or machine control is performed.
- Every completed upload is quarantined pending a local ClamAV scan. Only `clean`
files can be quoted, commercially approved, paid, downloaded, attached as final
files, or admitted to the print queue. Rejected/error files remain blocked and
expire within three days. The isolated scanner uses signatures bundled in its
pinned image and has no external network route. The transport accepts files up
to 5 GiB, but the local scan/release limit is 128 MiB; larger files remain
blocked. This malware gate is not print pre-flight or artwork validation.
- A retention worker removes expired object bytes but keeps order/file metadata:
incomplete uploads after one day, originals within seven days of manual final
artwork approval, and attached final/correction files within 30 days of the
order's first upload. Storage lifecycle is also a 30-day backstop.
- Structured security events are written to logs and PostgreSQL. The local
`security_status` command summarizes authentication, rate-limit, scan, and
scanner/signature alerts without exposing secrets. Security regression tests,
exact-runtime Python dependency auditing, and Trivy image reports live under
`app/` and `output/security/`; open findings are documented in
`SECURITY_REPORT.md` and are not a production-readiness claim.
- All direct and transitive Python packages are pinned with artifact hashes in
`infra/requirements.lock`; the API image build requires those hashes. The lock
is regenerated in a disposable Python 3.12 container by
`infra/lock_dependencies.sh`. A fresh exact-runtime audit found no known Python
advisories on 2026-09-15; this does not cover OS/container findings.
- `compose.staging.yaml` is a separate, network-disabled readiness gate only. It
validates non-secret staging decisions and rejects placeholders, local endpoints,
fake providers, embedded secret settings, and unsafe secret sources. It does not
deploy the application, contain credentials, or contact any real provider.
- A separate production delivery package now exists under `deploy/`, with
non-root API/web image definitions, a single Portainer Docker Swarm stack,
external secret/volume contracts, health-monitored rolling updates,
commit-SHA rollback images, and one protected Gitea workflow under
`.gitea/workflows/`. The package never contains provider credentials and has
not been deployed. Its fail-closed preflight intentionally rejects the current
source until production adapters, Docker-secret file loading, approved inputs,
restore rehearsal, image scans, and human security approval are complete.
- `python3 -m app.backup create-and-verify` creates a private, Git-ignored bundle
containing a PostgreSQL dump plus every complete, unexpired object already marked
`clean`. SHA-256 manifests protect both parts. Verification restores the database
under a UUID name and the object bytes under a UUID MinIO prefix, hashes the
restored bytes, then removes only those temporary targets. Active data is never
overwritten; pending, rejected, errored, expired, and purged objects are excluded.
Run this local snapshot while uploads and retention are idle. Scheduling, offsite
copies, and production restore operations remain unfinished.
- Real providers, production authentication hardening, email verification/recovery,
automatic final-file generation, production backup operations, and production
activation remain incomplete. Deployment plumbing exists but cannot pass its
release gate yet. No factory automation or pre-flight is added.
See `docs/historico/IMPLEMENTATION_REPORT.md` for verification, `PRODUCTION_INPUTS.md` for the
decisions and evidence required before staging or production connection, and
`PORTAINER.md` for the production delivery contract.
The client roadmap promises are unchanged, so its PDF source is not regenerated
for this local implementation checkpoint.
### Existing assets
- `web/`: the Site (`index.html`), the Kanban and customer portal pages, and
the scripts behind them. `web/site-*.js` holds the Site's behaviour.
- `docs/historico/`: the original specification, endpoint sketch, task plan and
status report. Background only — they describe a model this system does not
implement.
The prototypes they describe (`portal/`, `kanban/`, `agente/`, the root
`schema.sql` and `.env.exemplo`) were removed on 2026-09-21 once nothing
referenced them; recover from Git history if ever needed.
- `tools/generate_dtf_report.py`: generator for the client-facing roadmap PDF.
- `docs/roadmap-cliente.pdf`: the generated roadmap, as last sent.
- `docs/reuniao-2026-09-09-anotacoes.pdf`:
meeting notes that established the business direction. Treat it as context;
the latest decisions in this document define the active scope.
### Important gaps and stale assumptions
- The original Site had no backend payment, freight quote, order persistence,
authentication, purchase history, or real cart persistence. Local order
persistence, reviewed quotes, mock freight and fake payment now work through
`web/checkout.js`. The local customer portal now provides accounts and
order history; cart recovery is browser-local. Production account verification,
recovery, and cross-device cart editing are still absent.
- Without the local bridge, the original freight fallback remains a stub.
Real freight quotation remains unimplemented in all runtimes.
- The existing the removed prototypes duplicated WhatsApp senders and send
direct text messages. They are not production-ready notification modules.
- Older documentation and code describe a Tiny webhook creating an upload link,
a factory agent, local Kanban, FlexiPRINT automation, 90-day retention, and
12-month artwork reuse. Those are not the current MVP model.
## Documentation synchronization
`context.md` is the compact operating context for agents. The current
client-facing narrative, diagrams, and formatting live in
`docs/roadmap-cliente.pdf`, generated from `tools/generate_dtf_report.py`.
When a decision changes, update both the relevant implementation context here
and the roadmap source when it changes what the client-facing plan promises.
## Change-control rules for agents
- Preserve the current Site DTF commercial rules unless explicitly asked to
change them.
- Do not add factory-side automation, a local agent, hot folders, FlexiPRINT
control, machine dashboards, or a new post-MVP delivery commitment by
inference.
- Do not claim that R2 hosts or executes application services.
- Keep all customer-facing and internal documentation in English only when this
file is the requested artifact; otherwise follow the user's requested
language.
- When a requirement is unclear, distinguish between the MVP, a future
enhancement, and an existing prototype assumption before changing code.

370
docs/LOCAL_SETUP.md Normal file
View File

@@ -0,0 +1,370 @@
# DTF local development
Read `CONTEXT.md` first. The service lives in `app/`; older portal,
Kanban, agent, requirements, and SQL files are historical prototypes.
## Start
Install Docker Engine/Desktop with Compose v2 or later. Docker must be running
and your account must have permission to use it. The initial build downloads
public container images and Python packages; running integrations are local.
From this repository directory:
```bash
docker compose -f compose.local.yaml up --build
```
`docker-compose.yml` is the production/R2 stack and will not start locally; the
localhost stack is always `compose.local.yaml`.
No `.env` is required: Compose has the same disposable defaults as `.env.example`.
To customize, copy `.env.example` to `.env` and edit it. Never use real credentials.
For a detached start with readiness verification:
```bash
docker compose -f compose.local.yaml up --build -d --wait
docker compose -f compose.local.yaml ps
```
| Component | Local URL / port | Purpose |
|---|---|---|
| Site DTF | http://localhost:8080 | Existing Site with local checkout bridge |
| Customer portal | http://localhost:8080/portal.html | Local account, order history, corrections and files |
| Kanban | http://localhost:8081 | Quote review, orders, movement, original downloads |
| API health | http://localhost:8000/health | Checks PostgreSQL and MinIO |
| MinIO S3 | http://localhost:9000 | Direct signed multipart uploads and downloads |
| MinIO console | http://localhost:9001 | Inspect private local storage |
| PostgreSQL | `db:5432`, internal only | Shared persistent data |
| ClamAV | `scanner:3310`, internal only | Isolated malware scanner with bundled signatures |
| Worker | `worker:8002/health`, internal only | Mock outbox delivery, scanning and combined health |
Use `localhost` consistently; mixing it with `127.0.0.1` creates a different
browser session. Published ports bind only to `127.0.0.1`.
Kanban default login: `operator@example.test` / `local-operator-only`.
MinIO default login: `dtf_local` / `local-storage-only`.
These are public, disposable development values, not real credentials.
Interactive API documentation is disabled; `/docs` and `/redoc` are not local
operator interfaces.
## Browser test order
1. Open the Site. Choose **Arquivo por metro** and the manual/table-price path
(CDR/AI/PSD/TIFF). Select `tests/fixtures/local-test.cdr`. This is deliberately
harmless text for upload testing, not a printable CDR file.
2. Enter **1.01 metres**. The original calculation bills **1.10 m × R$19.90 =
R$21.89**, with grade 0 and pickup. You can also use your own non-sensitive
artwork with the original product controls, repetitions, and mixed cart.
3. Use test CNPJ `11.222.333/0001-81`, phone `11999999999`, email
`local-test@example.test`, and pickup. For simulated freight, select delivery,
enter an eight-digit CEP, and click quote. The default mock price is R$15.00.
4. Click the Site checkout button. Files upload directly to MinIO, remain
quarantined until ClamAV returns `clean`, and then become eligible for a quote.
The local banner displays a quote ID awaiting commercial review.
5. Open Kanban and log in. In **Cotações**, download the original if needed,
confirm/correct total metres and grade, tick the manual confirmation, and
click **Aprovar cotação**. For the fixture keep 1.01 m and grade 0.
6. Return to the Site and click its checkout action again. Inspect the
authoritative server total, then click **Criar pedido de teste**. No real
payment occurs.
7. Refresh Kanban. The paid order starts in **Arte recebida**. Move it using
the buttons or drag and drop to **Arte tratada**. Open **Arquivos de produção**,
select the manually prepared final files for every item, enter a review note,
tick the confirmation and click **Aprovar arquivos finais**. Use the harmless
fixture again only for this local test; no printable file is generated.
Continue through **Fila de impressão →
Imprimindo → Finalizado**. A move to **Correção** requires a reason; it can
return to **Arte recebida** or **Arte tratada**. Finalizado is terminal locally.
8. Inspect **Histórico** and **Eventos locais de integração**. Worker receipts
should say recorded locally. Download links expire after five minutes;
request another link from Kanban when needed.
The manual quote step is necessary to enforce the backend trust boundary while
automatic pre-flight is deferred. Browser measurements and grade are proposals.
Only an authenticated operator can approve them; the server derives prices and
freight. Payment accepts a quote ID only. Approved quotes are immutable for
24 hours, and repeated/concurrent payment requests return the same order.
This local review step does not settle the future automated production checkout.
Malware scanning is a separate safety gate and does not inspect dimensions,
resolution, colors, printability, or any other pre-flight property.
The existing client-side previews/analysis remain unchanged and advisory. No new
automatic print pre-flight or artwork validation runs on upload; malware scanning
is the separate gate described above. Original downloads and manually approved
final files are separately labeled. Never print the local text fixture.
## Customer accounts, corrections, and cart recovery
Open **Minha conta** on the Site, or `/portal.html`. Create a local account with
a test CNPJ, phone, email and password of at least 12 characters. Passwords use
the current stronger scrypt format; legacy local hashes are verified and upgraded
on successful login. Sessions are stored in PostgreSQL, expire after
seven days, and are revoked on logout. Login/registration attempts are limited.
No email service is used: email verification and password recovery remain absent.
Registration associates only the current guest session's uploads, quotes and
orders with the new account. Signing in from another browser restores access to
that account's orders. Matching an email or CNPJ never grants access to an order.
Guests can still order and track within their current session.
In the portal, **Ver detalhes e arquivos** displays status history, correction
reasons, files and expiry dates. When Kanban requests **Correção**, upload corrected
files against the relevant item and add a note. The order stays in correction
until the operator reviews it. The old final set is invalidated; the operator
must upload/approve another complete set before re-entering the queue. Concurrent
or repeated submissions with a stale order version are rejected.
The browser saves unfinished cart items and their File blobs in IndexedDB for
24 hours. Reloading restores them as cart rows; remove/replace a row to alter its
artwork settings, or add more products. Existing calculations are preserved and
delivery must be requoted. Payment clears the saved cart. Cart recovery is local
to the browser, not a cross-device artwork library; very large files can exceed
browser storage capacity, which is reported visibly. Expired browser entries are
removed the next time the Site opens. Server retention does not erase downloaded
files or copies stored by the browser/operating system.
Logout also clears local checkout keys and IndexedDB File blobs across open Site
tabs. Operator authentication uses an expiring, revocable HttpOnly session;
browser storage never retains the operator password or a Basic-auth credential.
## Automated checks
Pricing parity requires Python 3.10+ and Node 22+ on the host, no package install:
```bash
python3 -m unittest local.test_pricing -v
```
This executes the real pricing constants/functions extracted from `web/site-config.js`
and compares all four modes, 101 grades, and 11 lengths (4,444 cases) to backend
pricing, plus assembly and invalid-input checks.
With the stack healthy, run the integration test (Python standard library only):
```bash
python3 -m tests.smoke_test
python3 -m tests.workflow_test
```
It checks direct two-part upload/resume, missing parts, session isolation,
private downloads and byte identity, customer validation, tampered totals,
operator corrections, all four products, freight, immutable quotes, concurrent
payment retries, state rules, history, and durable fake integration receipts.
Each run leaves one clearly labeled test order and an approximately 8 MiB object.
The workflow test also checks guest-to-account migration, cross-session login,
revoked sessions, ownership isolation, correction submissions, final revision
invalidation, secure downloads, and mandatory final-file approval before queueing.
Security and malware regressions are separate so an authorized harmless EICAR
test is unmistakable:
```bash
python3 -m tests.security_test
python3 -m tests.scanning_test
docker compose exec -T api python3 -m tests.runtime_security_test
docker compose exec -T api python3 -m tests.retention_test
```
`local.scanning_test` stores an EICAR fixture as `SECURITY-EICAR.cdr`; ClamAV
must reject it, and quote/download gates must remain closed. The rejected fixture
is retained for at most three days, so it temporarily appears in alert summaries.
For an automated real-browser walkthrough, install Chrome and use Node 22+:
```bash
node tests/browser_test.mjs
```
Set `CHROME_BIN` if Chrome is not at `/usr/bin/google-chrome-stable`. The test
uses a separate temporary browser profile, walks through the actual Site and
Kanban controls, and saves screenshots to `output/local/`. It leaves its local
test order for inspection. Both integration scripts read `.env` automatically.
## Configuration and storage
`.env.example` lists local ports, database/MinIO values, operator login, adapter
selection, mock freight amount, maximum file size (5 GiB), and multipart size
(8 MiB by default). The malware scanner releases only files up to 128 MiB by
default (`SCAN_MAX_BYTES`); larger uploads remain blocked even though the
multipart transport supports 5 GiB. `S3_ENDPOINT=http://storage:9000`, database
hostname `db`,
and the internal service ports are fixed Compose wiring. The public S3 endpoint
must resolve from the browser; keep `http://localhost:9000` for this stack.
Parts use 15-minute presigned URLs and uploads must finish within one day.
Only fake integration adapters and `s3-local` storage are accepted. Startup fails
if a production adapter/environment or nonlocal S3 endpoint is selected.
The API, worker, and database run on an internal Docker network; web gateways
and MinIO also join a network that permits loopback port publishing.
Objects are private, use UUID keys rather than filenames, and persist in a named
volume. MinIO lifecycle rules expire objects after 30 days and abandon incomplete
multipart uploads after one day; the API also blocks expired downloads. Order
history remains in PostgreSQL. Completed files start in `pending`; unknown,
scanner-error, over-limit, encrypted/unsafe, and malware results fail closed.
Only `clean` files can cross quote, payment, download, final-approval, and queue
gates. Rejected/error objects expire within three days. The worker deletes
original bytes within seven days of
manual final artwork approval, and final/correction bytes by 30 days from the
order's first upload. Expired files remain visible as metadata but cannot be
downloaded. Retention runs once a minute; MinIO lifecycle is a backstop.
Upload retries resume completed parts. The saved cart retains files when browser
storage permits; otherwise reselect them. Saved quote IDs also survive reloads.
Clearing cookies loses a guest session, while registered customers can sign in
again. The operator can still inspect order records.
## Local backup and restore check
```bash
python3 -m app.backup create-and-verify
```
This writes a private four-file bundle in `backups/` (ignored by Git and Docker
builds): a PostgreSQL custom-format dump, a gzip object archive, a JSON manifest,
and the manifest's SHA-256 sidecar. The object archive includes only complete,
unexpired files that ClamAV has marked `clean`; pending, rejected, errored, expired,
and purged objects remain excluded. Run the command while uploads and retention are
idle so the database dump and subsequent object selection describe the same local
state.
Verification restores the dump into a newly created UUID-named database and the
object bytes under a UUID-named `originals/restore-verification/` MinIO prefix. It
checks database counts, bundle hashes, every archived object's hash, and the bytes
downloaded after restore, then removes only the temporary database and objects. It
never restores over active data. Keep every bundle file private: it contains
customer data, password hashes, and customer artwork. To verify it again, run
`python3 -m app.backup verify` followed by the printed
`backups/...manifest.json` path. Legacy database-only `.dump` backups remain
verifiable. Scheduling, offsite copies, and a production restore runbook remain
unfinished.
After building the current image, test retention with synthetic files:
```bash
docker compose exec -T api python3 -m tests.retention_test
```
This checks that expired bytes are removed while unexpired files survive. It
cleans up its own synthetic object bytes and retains their metadata.
## Operations and troubleshooting
```bash
docker compose logs --tail=100 api worker scanner
docker compose restart api worker
docker compose ps
docker compose down
```
`down` stops the stack and preserves named database/storage volumes. Restart
with `docker compose up -d --wait`. Do not add `--volumes` unless you intend to
permanently erase all local orders and artwork. No reset is required for tests.
Seven long-running services have Docker health checks; the database and storage
initialization jobs must exit successfully. API health queries PostgreSQL and
MinIO; worker health fails if its database-processing loop, scanning thread, or
live ClamAV PING is unavailable. Both web gateways expose `/health`. MinIO
exposes `/minio/health/ready`.
Run the redacted local alert summary inside the API network namespace:
```bash
docker compose exec -T api python3 -m app.security_status
```
Exit status 1 means attention is required. Review blocked artwork, rate limits,
failed logins, scanner availability, and signature age. Immediately after the
security tests, expected synthetic login failures/rate limits and
`SECURITY-EICAR.cdr` rejections will trigger the alert; confirm names and test
timing before classifying them as expected. Unexpected failures, scanner errors,
stale signatures (more than seven days), or non-test rejected files require
investigation. Events are retained for 30 days. See `SECURITY_REPORT.md`.
If a host port is occupied, change `SITE_PORT`, `KANBAN_PORT`, or `API_PORT` in
`.env` and recreate the stack. MinIO currently reserves 9000/9001. If Docker says
permission denied, fix Docker access or use your OS-approved Docker workflow.
If image/package download fails, check development internet access. A warning
about missing Buildx can fall back to Docker's classic builder; install your
platform's Buildx plugin for the supported modern build path.
If API is unhealthy, inspect its logs and check local adapter settings. Database
and MinIO credentials initialize persistent volumes only once; changing values
later also requires updating the existing database/storage account deliberately.
If an upload fails, keep the session and click checkout again. If parts expired,
reselect the file to start another upload. For a stale Kanban move, refresh;
the backend rejects stale versions instead of overwriting another operator.
The Site retains its existing remote visual assets (fonts/logo) and optional
PDF.js 3.11.174 CDN dependency. Its known eval-based advisory is mitigated in
the current call with `isEvalSupported:false`; the old CDN dependency is still
an upgrade/vendor-pinning limitation, not a claim that it is current. The local
API, manual fixture flow, storage, payment,
freight, Kanban, and mock integrations work without these external services.
Offline PDF previews may fall back to the prototype's manual/table-price path.
## Dependency lock
The API, worker, and database initializer install every Python dependency from
`infra/requirements.lock` with `--require-hashes`. `infra/requirements.txt`
remains the human-maintained direct dependency list. After deliberately changing
a direct pin, regenerate the lock in the same Python 3.12 environment and rebuild:
```bash
./infra/lock_dependencies.sh
docker compose up --build -d --wait
```
The generator downloads public package metadata in a disposable container and
does not modify the host Python environment. Review the resolved versions and run
the exact-runtime audit plus regression suite before accepting an updated lock.
## Staging readiness gate
`compose.staging.yaml` is intentionally not a staging deployment. It runs only a
network-disabled validator for non-secret decisions. After completing
`PRODUCTION_INPUTS.md`, copy `staging/staging.env.example` to the ignored
`staging/staging.env`, enter non-secret metadata, and run:
```bash
docker compose -f compose.staging.yaml run --rm readiness
```
A pass does not authorize deployment or prove provider access. The real staging
composition and external secret injection still require approved provider
contracts and owners. See `staging/README.md`.
## Next production connection step
Keep this stack local. Before connecting real services, confirm the production
checkout trust workflow, complete `PRODUCTION_INPUTS.md`, and pass the isolated
staging-readiness gate. Then implement the actual staging composition using the adapter contracts in
`app/adapters.py`: start with private S3 staging storage, CORS, signed multipart
contract tests and scoped credentials injected outside Git. Add provider sandbox
adapters one at a time. Mercado Pago requires authenticated, signed, idempotent
webhook handling before real payment is allowed; Tiny/Olist and WhatsApp need
confirmed contracts and provider idempotency/delivery handling. Do not simply
change the local fake flags or point this Compose file at production.
## Production delivery package
The repository now includes a separate Docker Swarm and Gitea Actions delivery
package in `deploy/` and `.gitea/workflows/`. It is not used by this localhost
Compose stack and does not alter its volumes. Production images run as
unprivileged users and install the hash-locked Python runtime. Gitea publishes
`latest` for the normal Portainer webhook plus the full commit SHA for rollback.
The stack expects pre-provisioned external Swarm secrets and an external
PostgreSQL volume. Only Site and Kanban ports are published for the existing TLS
reverse proxy; API, database, worker, and scanner remain private.
Run the source-only gate without credentials:
```bash
python3 deploy/production_preflight.py --source-only
```
It must remain blocked while `app/` supports only local fake adapters and does
not load Docker secret `*_FILE` settings. Do not bypass or delete this check.
After approved production implementations and inputs exist, follow
`PORTAINER.md` and `deploy/PRODUCTION_CHECKLIST.md`; release and deployment
still require the Gitea scan/test gates and explicit approvals.

156
docs/PORTAINER.md Normal file
View File

@@ -0,0 +1,156 @@
# Portainer deployment
DTF follows the same operating model as Graphs and ComporHUB: Gitea builds
prebuilt images, pushes them to the Gitea registry, and calls one Portainer
webhook. Portainer owns and redeploys one Docker Swarm stack named `dtf-cloud`.
The deployed stack is the repository's `docker-compose.yml`, which the
`dtf-cloud` Portainer stack points at. It was once accompanied by `deploy/stack.yaml`, a more hardened definition
supplying credentials as Docker secrets; that file was removed in favour of one
definition. See `ROADMAP.md` 2.12 for the reasoning and what remains open.
The stack contains Site, Kanban, API,
worker, PostgreSQL, ClamAV, and a one-time database initializer. Production uses
Cloudflare R2, so MinIO is not part of this stack.
## 1. Gitea configuration
The single workflow is `.gitea/workflows/deploy.yml`. Every push and pull request
runs static validation, the integration suite against a real stack, and a secret
scan. A push to `main` then builds the production images, publishes both `latest`
and the full commit SHA, reports their vulnerabilities, and calls Portainer.
What actually gates a deployment:
| Check | Gates? |
|---|---|
| `py_compile` and the unit tests | yes |
| Integration suite on a live stack (smoke, workflow, security, scanning, retention, runtime) | yes |
| Browser suites | only when the runner has Chrome; otherwise warns and continues |
| Trivy secret scan (HIGH/CRITICAL) | yes |
| Source preflight (`deploy/production_preflight.py --source-only`) | only when `ENFORCE_PRODUCTION_PREFLIGHT` is `true` |
| Trivy image vulnerabilities, CRITICAL | yes |
| Trivy image vulnerabilities, HIGH | no — reported after the build |
The source preflight is advisory by default because it refuses a release while
the payment and messaging adapters are fake, which is the deliberate state the
stack runs in today. Enforcing it now would block every deployment. Set the
repository variable `ENFORCE_PRODUCTION_PREFLIGHT` to `true` once real adapters
land, and it becomes a hard gate.
CRITICAL image findings block. Both images carry none: the bases are pinned by
digest, both Dockerfiles upgrade their OS packages, and the web image moved off
the 1.28 nginx line, which pins `nginx=1.28.3-r1` in `/etc/apk/world` and so
cannot be patched in place. HIGH findings are reported rather than enforced
because the remainder have no upstream fix.
`PYTHON_BASE_IMAGE` and `NGINX_BASE_IMAGE` override the digests pinned in the
Dockerfiles. Update the Dockerfile default in the same change, so the repository
still records what a build used.
Repository variables:
- `REGISTRY_HOST` — normally `gitea.blyzer.com.br`.
- `REGISTRY_OWNER` — normally `blyzer`.
- `PYTHON_BASE_IMAGE`, `NGINX_BASE_IMAGE`, `TRIVY_IMAGE` — approved immutable
`@sha256:` image references.
Repository secrets:
- `REGISTRY_USERNAME` and `REGISTRY_TOKEN` — package write credentials.
- `PORTAINER_WEBHOOK` — webhook generated by the `dtf-cloud` Portainer stack.
The webhook is called only after the gating checks in the table above pass.
`ENFORCE_PRODUCTION_PREFLIGHT` and `TRIVY_IMAGE` are optional repository
variables; without them the preflight is advisory and a pinned default scanner
image is used.
## 2. One-time Portainer resources
Use a Docker Swarm environment. Create a dedicated production PostgreSQL volume
and set its name as `POSTGRES_VOLUME`. Label its Swarm node
`dtf_database=true`. Do not reuse the localhost Compose volume.
Create each application credential under **Secrets**. Use versioned names such
as `dtf_prod_database_url_v1`, then place only those names in the corresponding
`*_SECRET` stack variables. The stack expects distinct secrets for:
- runtime and administrator database URLs;
- database administrator and application-role passwords;
- R2 access key and secret key;
- operator password;
- Mercado Pago token and webhook secret;
- Tiny/Olist token;
- WhatsApp token.
Credential values must never be entered into Git, `portainer.env`, or Gitea
workflow variables.
## 3. Create the stack once
In Portainer select **Stacks → Add stack → Git repository**:
- Name: `dtf-cloud`
- Repository: this Gitea repository
- Reference: `main`
- Compose path: `docker-compose.yml` (the repository default, which is what the
existing `dtf-cloud` stack uses; it must stay at the root for this reason)
- Registry: the private `gitea.blyzer.com.br` registry
- Re-pull image: enabled
- Automatic update: webhook
Load a completed copy of `deploy/portainer.env.example` as the stack's
non-secret environment variables. Do not load the example unchanged: `TBD` and
zero digests are deliberate blockers. Keep `IMAGE_TAG=latest` for normal
webhook deployments.
Before entering those values in Portainer, validate the completed ignored file:
```bash
set -a
. deploy/portainer.env
set +a
python3 deploy/production_preflight.py
```
The public reverse proxy should send the Site hostname to `SITE_PORT` and the
Kanban hostname to `KANBAN_PORT`. `/api/` and `/portal.html` are served through
the Site gateway. Preserve the original Host header. Do not expose PostgreSQL,
API, worker, or ClamAV. Restrict the
two published web ports at the host firewall so only the reverse proxy can use
them, and terminate HTTPS at the proxy.
The `db-init` service completing and stopping is expected. The other six
services must be healthy. A failed `db-init` task or an unhealthy service blocks
acceptance.
## 4. Normal deployment
Push to `main`. Gitea validates, tests, scans, publishes these images, and calls
the webhook:
```text
gitea.blyzer.com.br/blyzer/dtf-api:latest
gitea.blyzer.com.br/blyzer/dtf-api:<full-commit-sha>
gitea.blyzer.com.br/blyzer/dtf-web:latest
gitea.blyzer.com.br/blyzer/dtf-web:<full-commit-sha>
```
In Portainer, verify the new image digests, service health, `db-init` result, and
the public `/health` endpoints. Back up PostgreSQL and R2 before changes that
affect stored data or retention.
## 5. Rollback
In the Portainer stack variables, change `IMAGE_TAG` from `latest` to the full
SHA of the last known-good Gitea commit and redeploy. This rolls back Site, API,
Kanban, and worker code; it does not undo database migrations or restore data.
After recovery and a corrected release, return `IMAGE_TAG` to `latest`.
## Current blocker
This is the final deployment shape, but it is not authorized for production
today. Production adapters, Docker-secret file loading, provider sandbox tests,
current clean base images, approved inputs, and a production restore rehearsal
are still required. Do not bypass `deploy/production_preflight.py` or the Gitea
scan gates to make a deployment run.

62
docs/PRODUCTION_INPUTS.md Normal file
View File

@@ -0,0 +1,62 @@
# DTF staging and production input checklist
Status: input worksheet only. Nothing in this document authorizes a real
integration, production deployment, or use of production credentials.
Do not put passwords, tokens, webhook secrets, private keys, customer data, or
provider recovery codes in this repository. Record only the credential owner and
the approved secret-injection location. Test every provider in an isolated staging
or sandbox environment before production activation.
## Decisions required before staging implementation
| Area | Required decision or evidence | Owner | Status/date |
|---|---|---|---|
| Artwork acceptance | Decide whether customer-entered length and quality grade are accepted directly, manually approved, or verified by another defined process. Name the commercial authority and rejection/correction flow. | | |
| Checkout | Approve the exact transition from quote to payment, including quote expiry, customer confirmation, cancellation, refund, and correction rules. | | |
| Infrastructure | Confirm VPS capacity, operating system, domain/subdomain, DNS owner, TLS termination, Portainer access, Gitea Actions path, and deployment/rollback owner. | | |
| Cloudflare R2 | Provide a staging bucket and endpoint, CORS policy, retention/lifecycle rules, narrowly scoped credential owner, storage budget, and restore/rollback acceptance test. | | |
| PostgreSQL | Define the managed/self-hosted choice, encryption and access policy, backup destination, schedule, retention, restore objective, and named restore-test owner. | | |
| Malware signatures | Approve how ClamAV signatures are refreshed without unrestricted scanner egress, plus stale-signature and scanner-outage response. | | |
| Freight | Select the source-of-truth platform, staging access, origin address/postal code, services/carriers, packaging weight/dimensions by DTF length, pickup rules, and pass-through/subsidy/free-shipping policy. | | |
| Mercado Pago | Provide sandbox account ownership, webhook administration, approved event/status mapping, refund/cancellation policy, reconciliation owner, and secret-injection location. | | |
| Tiny/Olist | Confirm API version/endpoints, staging access, order/product/customer mappings, tag/marker behavior, idempotency key, rate limits, retry rules, and reconciliation owner. | | |
| WhatsApp | Select the provider, sending number owner, opt-in/legal basis, approved templates for the four agreed events, staging access, retry/delivery-failure policy, and support owner. | | |
| Customer accounts | Select email verification and password-recovery provider/flow, session policy, privacy/contact requirements, and customer-support ownership. | | |
| Operators | Define named operator provisioning/removal, roles, MFA expectation, emergency access, and periodic access review. Shared production credentials are not acceptable. | | |
| Monitoring | Name alert recipients and escalation windows for authentication abuse, malware detections, stale signatures, queue failures, provider failures, backup failures, capacity, and downtime. | | |
| Security findings | Record remediation or explicit risk acceptance for the current MinIO, PostgreSQL, Nginx, Debian, and PDF.js findings before a production-readiness review. | | |
## Evidence required before production activation
- A separate staging composition/deployment with no production secrets and no
route from local fake integrations to real provider accounts.
- Server-authoritative pricing regression results, including all four product
modes, discounts, assembly charges, minimum length, rounding, and freight.
- Direct multipart R2 tests for resume, exact part sizes, private access, CORS,
expiry, abort, malware quarantine, secure download, and retention.
- Signed and idempotent Mercado Pago webhook tests, including duplicate, delayed,
invalid, cancelled, and refunded events. No payment may be created before freight
is final.
- Idempotent Tiny/Olist and WhatsApp outbox tests covering retry, duplicate delivery,
rate limiting, permanent failure, and operator reconciliation.
- Account verification/recovery, operator access, TLS, cookie, Host/origin, audit,
alert, and incident-response checks in the target architecture.
- A scheduled, encrypted, offsite PostgreSQL/object backup and a documented restore
rehearsal. The verified localhost bundle is useful evidence, not this production
control.
- A recorded go/no-go review signed by the business owner, operations owner, and
technical owner, with rollback contacts and a support window.
## Safe implementation order after inputs are approved
1. Complete the non-secret metadata and pass the network-disabled readiness gate.
2. Create the actual separate staging composition and external secret-injection path.
3. Validate private R2 multipart storage, malware release gates, downloads,
retention, backup, and restore.
4. Add freight quotation because its final value is required before payment.
5. Add Mercado Pago sandbox checkout and authenticated idempotent webhooks.
6. Add Tiny/Olist through the existing outbox/idempotency boundary.
7. Add only the four agreed WhatsApp notification events.
8. Run the complete functional, security, dependency, image, recovery, and manual
acceptance suite in staging before any production activation.

585
docs/ROADMAP.md Normal file
View File

@@ -0,0 +1,585 @@
# DTF System — Working Roadmap
> Internal engineering tracker. Not a client document, not a promise sheet.
> The client-facing narrative lives in `docs/roadmap-cliente.pdf` and in the weekly
> report, which is a deliverable and is not versioned here.
>
> Update the **Current step** line and the item status every time something moves.
> Add new findings at the bottom of the relevant block rather than rewriting history.
**Current step:** Block 0 closed; Block 2 closed except 2.9–2.11; 4.1, 4.2, 4.5,
5.1, 5.3, 5.4 and 5.8 done. Remaining work needs decisions (3.1, 3.2, 3.3) or
client inputs (1.1, 1.2, 2.10). Block 1 still waits on client inputs for
1.1/1.2.
> Paths in closed items are written as they were when the finding was made.
> The repository was laid out by role on 2026-09-21 (`local/` became `app/`,
> with `tests/`, `ops/`, `infra/` and `web/` beside it); the history is left
> as recorded rather than rewritten.
**Last audit:** 2026-09-18, full read of `app/`, `dtf-site.html`, `deploy/`,
`.gitea/`, docs and legacy prototypes. Findings below carry their audit IDs.
| Status | Meaning |
|---|---|
| `[ ]` | not started |
| `[~]` | in progress |
| `[x]` | done and verified |
| `[?]` | blocked on a decision (product or client), not on code |
---
## Block 0 · Broken right now
Nothing in this block is optional. Until it is closed, the system cannot be
demonstrated, and the week-1 claims cannot be defended.
### `[x]` 0.1 — API returns 500 on every session, login and registration `(F1)`
`local/auth.py:46` calls `os.environ` and the module never imports `os`.
Reproduced: `NameError: name 'os' is not defined`. `/api/session` calls
`new_session()` whenever there is no cookie, so the Site checkout bridge, cart
recovery and the customer portal all fail on first visit. Introduced in `e3e37f6`.
- Add `import os` to `local/auth.py`.
- Move the `COOKIE_SECURE` read to a module constant so it is evaluated once.
- **Accept:** a fresh browser hits the Site and `/api/session` returns 200 with a
`cart_scope`; `local/smoke_test.py` and `local/workflow_test.py` pass.
### `[x]` 0.2 — "Arquivo por metro" is unreachable in practice
Two independent causes, both from 2026-09-18 commits. Verified in a browser.
**a. Every entry point hard-routes to loose artwork** (`483a083`)
| Control | Currently opens |
|---|---|
| Nav "Impressão DTF" | `avulsa` |
| "DTF Têxtil · 57 cm" | `avulsa` |
| "DTF UV · 28,5 cm" | `uv` |
| Hero "Enviar minha arte" | `avulsa` |
Only the price card reaches `file`. Revert the three `data-modo-cta` attributes
so navigation links land on the product chooser, not on a product.
**b. Dropping a PNG/JPG in `file` mode silently switches the order** `(F25)`
`dtf-site.html` `sel()` — from `abrir('file')`, dropping `imagem-teste.jpg` gives
`modo: "avulsa"`, header "Artes avulsas", price `R$ 29,90/m`. Meanwhile the same
screen says *"Arraste suas folhas montadas · PNG, JPG ou PDF"*, marks
*"PNG, JPG ou PDF · a partir de R$ 14,90"* as the recommended path, and sets
`input.accept=".png,.jpg,.jpeg,.pdf"`. The page invites the drop and then
reprices the order 50% higher.
The escape hatch `#imagemComoFolha` sits above the drop zone as small text inside
an informational notice, defaults to unchecked, and must be ticked *before* the
drop. After the switch fires, `pintaModo()` sets `trocarParaAvulsa.hidden = true`,
so the checkbox disappears and there is no way back in place.
Net effect: the only formats that survive `file` mode are PDF/TIFF/PSD/AI/CDR —
the path the UI itself marks as the worse option. A mixed drop (JPG + PDF) is
rejected and accepts nothing.
- Make the ready-sheet choice an explicit two-option control **inside** the drop
area, styled like the existing `.cam` selector — not a checkbox in a notice.
- Stop advertising PNG/JPG in the by-metre drop zone while rejecting them.
- When a switch does happen, show the price change and offer one-click undo.
- **Accept:** a customer can complete a by-metre order with a PNG from any entry
point, and no product/price change ever happens without a visible confirmation.
### `[x]` 0.3 — Ready-sheet declaration is unverified and worth money `(F22 related)`
Ticking `#imagemComoFolha` is an honour-system claim that moves the price from
R$ 29,90/m to R$ 19,90/m (R$ 14,90 with a good grade). `medirFolha` then derives
sheet height purely from aspect ratio × 57 cm — the original bug, now opt-in.
Measured with `imagem-teste.jpg` (466 × 659 px):
| Route | System behaviour | Billed |
|---|---|---|
| Ticked | treated as a 57 × 80,6 cm mounted sheet | 1 m × R$ 19,90 = **R$ 19,90** |
| Not ticked | 20 cm wide, packs to 28,3 cm of film | 1 m × R$ 29,90 = **R$ 29,90** |
- Validate the claim: declared width must be ≈ film width (57 / 28,5 cm) at a
plausible DPI before the sheet model is accepted.
- Re-check server-side in `/api/operator/quotes/{id}/approve` before pricing.
- **Accept:** a small single artwork declared as a ready sheet is rejected with a
clear message; a genuine 57 cm sheet passes; the operator sees the verdict.
### `[x]` 0.4 — Documented local startup fails `(F2)`
`LOCAL_SETUP.md` says `docker compose up --build` with no `.env`.
`docker compose --env-file .env.example config` exits 1: `R2_ENDPOINT`,
`R2_ACCESS_KEY_ID`, `SITE_DOMAIN`, `KANBAN_DOMAIN` missing. `docker-compose.yml`
became a production/R2 stack in `e3e37f6`; `.env.example` is still the MinIO one
and there is no MinIO service left.
- Decide: keep one production compose and add `compose.local.yaml` with MinIO, or
restore a local default. Recommend the former.
- **Accept:** a clean clone reaches a working Site + Kanban with the documented
command, and `LOCAL_SETUP.md` matches what actually runs.
### `[x]` 0.5 — App DB role shares the admin password `(F3)`
`docker-compose.yml:65` sets `APP_DB_PASSWORD: ${POSTGRES_PASSWORD}` — the same
value as `dtf_admin`. `bootstrap.py` grants the app role DML-only and then hands
it a credential that also logs in as the owner. Anyone reading the API container
env has admin on the database.
- **Accept:** distinct secrets; connecting as `dtf_app` with the admin password fails.
### `[x]` 0.6 — A paid order showed the customer nothing (found while closing Block 0)
`#checkoutStatus` and `#checkoutActions` lived inside `#carr`, and the success path
in `checkout.js` clears the cart (`pedido=[]; limpaPaineis()`) before writing the
confirmation — `.carr{display:none}` then hid the panel holding it. Present since
the first commit; it only surfaced once 0.1 made a payment reachable at all.
Both elements now sit in their own always-visible `.checkout` container.
### How Block 0 was verified
A live stack (`compose.local.yaml`), then the full suite:
| Check | Result |
|---|---|
| `/api/session` on a cold browser | 200 with `cart_scope` + session cookie |
| smoke · workflow · security · scanning | pass |
| retention · runtime security (in-container) | pass |
| `artwork_browser_test.mjs` | pass, updated to the new declared-product behaviour |
| `browser_test.mjs` end-to-end | pass — upload → quote → operator approval → paid order → all Kanban states |
| 4 CI unit tests | pass |
Ports 8090/8091/8010 were used; 8080 was held by an unrelated preview server.
---
## Block 1 · Week 2 — committed to the client
From the report already sent. These are dated promises, not backlog.
- `[ ]` 1.1 — Mercado Pago transparent checkout, signed and idempotent webhooks.
Requires production credentials + webhook access. Payment must never be created
before the freight amount is final.
- `[ ]` 1.2 — Real freight quotation. **Blocked on client inputs** (see
`PRODUCTION_INPUTS.md`): source platform, credentials, origin CEP, services,
packaging weight/dimensions per length, subsidy policy.
- `[ ]` 1.3 — Idempotent Tiny/Olist order creation with order-number traceability.
Confirm endpoints, tag behaviour and rate limits first.
- `[ ]` 1.4 — Final print-file generation (see 3.2 — this is the same problem).
- `[ ]` 1.5 — Main Kanban production states consolidated.
- `[ ]` 1.6 — **Block 0.2 + 0.3**, promised as "início da próxima semana".
`[!]` The production compose currently blocks `dev_paid` (`ENVIRONMENT != 'local'`)
and ships only fake adapters, so the deployed system cannot take an order at all.
1.1 is what unblocks it.
---
## Block 2 · Security — before any public exposure
### `[x]` 2.1 — Rate limiting and audit logs are blind to the client `(F5)`
uvicorn runs without trusted proxy headers (`forwarded_allow_ips` defaults to
`127.0.0.1`; nginx is a different container IP), so `request.client.host` is nginx
for every request. `rate_limit('auth-source', ...)` at 60/15min becomes a single
global bucket — **60 failed logins lock out every customer** — and every `audit()`
record has no attacker IP.
- Set `--proxy-headers` with `FORWARDED_ALLOW_IPS` scoped to the nginx service, or
read `X-Forwarded-For` explicitly at the edge.
- **Accept:** two clients on different IPs have independent buckets; audit rows
carry the real IP.
### `[x]` 2.2 — The public site throttles itself `(F6)`
`local/app.py:115` — `rate_limit('guest-sessions', ENVIRONMENT, 120, 900)` is keyed
on the environment name: 120 new visitors per 15 minutes **site-wide** (~8/min).
Normal traffic 429s. Key per source IP (after 2.1) and raise the ceiling.
### `[x]` 2.3 — `deploy/stack.yaml` cannot boot `(F7)`
It passes `DATABASE_URL_FILE`, `AWS_ACCESS_KEY_ID_FILE`, `OPERATOR_PASSWORD_FILE`,
`OPERATOR_USER`. The code reads `DATABASE_URL`, `AWS_ACCESS_KEY_ID`,
`OPERATOR_PASSWORD`, `OPERATOR_EMAIL`, and no `_FILE` loader exists.
`app.py:58` does `os.environ['OPERATOR_PASSWORD']` → `KeyError` → 500 instead of 503.
- Implement `local/secrets.py` (the preflight already expects it) reading `*_FILE`
with env fallback. Reconcile `OPERATOR_USER` vs `OPERATOR_EMAIL`.
- **Accept:** the stack renders and boots against Swarm secrets; missing operator
config yields 503, not 500.
### `[x]` 2.4 — The documented release gate does not exist `(F8)`
`PORTAINER.md` and `SECURITY_REPORT.md` claim the workflow runs the full isolated
suite, Trivy HIGH/CRITICAL image gates, secret scanning and the source preflight
before calling Portainer. `.gitea/workflows/deploy.yml` runs `py_compile` plus four
unit tests, then builds, pushes `latest` and calls the webhook **unconditionally**.
`deploy/production_preflight.py` is never invoked — only its unit test runs.
- Either implement the gate or correct both documents. Do not leave the gap.
### `[x]` 2.5 — The preflight has silently decayed `(F9)`
It blocks by string-matching source. **4 of 6 markers are dead** after the R2
refactor: `'This runtime only supports APP_ENV=local'`,
`'Only local S3 storage is supported'`, `"allowed_hosts=['localhost', '127.0.0.1']"`,
`"'environment': 'local'"`. String gates weaken without failing.
- Replace marker matching with behavioural assertions (import the module, assert
the adapter classes in use).
### `[x]` 2.12 — Two divergent stack definitions; the docs named the wrong one
Found 2026-09-21 by asking which file Portainer deploys. `PORTAINER.md` called
`deploy/stack.yaml` "the production stack"; the deployed file is the repository's
`docker-compose.yml`. `deploy/stack.yaml` came from the first commit and was never
deployed — it supplied credentials as Docker secrets where the deployed file uses
plain environment variables.
**Decision (2026-09-21): keep `docker-compose.yml`, delete `deploy/stack.yaml`.**
The gain from Docker secrets here is narrower than it sounds. It keeps values out
of `docker inspect` and the Portainer UI, but `local/secrets.py` loads them into
the process environment anyway, and anyone who can read `docker inspect` is
already root or in the docker group and could read the secret files directly. The
operator is the only Portainer user, so the main benefit — limiting what a
lower-privileged console user can see — does not apply. Maintaining two
definitions that drift was the larger real cost.
`local/secrets.py` stays. It is inert against the deployed file and costs nothing,
and it means a stack can switch to Docker secrets later without a code change.
Still open: **rotate the R2 secret key.** Not because of Portainer, but because it
grants read and write over every customer's artwork and has been readable from the
stack environment for some time. The operator password is worth rotating with it.
### `[x]` 2.6 — Base images are not pinned `(F10)`
Dockerfiles default to mutable `python:3.12-slim` / `nginx:1.28-alpine`, the
workflow passes no digest build-args, and `--pull` makes builds non-reproducible —
while `PORTAINER.md` documents digest-pinned immutable bases.
### `[x]` 2.7 — pdf.js loaded from CDN without integrity `(F11)`
Vendored rather than integrity-pinned, so the Site no longer depends on a third
party being reachable and honest when a customer opens it. Both files are served
from this origin and their provenance is recorded in `local/static/vendor/README.md`,
verified against the SRI digests cdnjs publishes for 3.11.174.
`cdnjs.cloudflare.com` is gone from `script-src`, `worker-src` and `connect-src` in
both gateway templates: scripts and workers are now `'self'` plus `blob:` for the
worker the Site builds itself.
Verified in a browser against the running stack: pdf.js loads from `/vendor/`,
the blob worker starts, and a real 7-page PDF parses with no CSP violation. Both
browser suites and the full integration suite pass.
**Still open: the version.** 3.11.174 is old. GHSA-wgrm-67xf-hhpq is mitigated —
`dtf-site.html` already passes `isEvalSupported: false`, which is the documented
workaround — but staying on it indefinitely is not a posture. Upgrading is an API
change rather than a file swap and needs its own browser testing, so it is
deliberately not bundled here.
### `[x]` 2.8 — Single shared operator credential `(F12)`
Accounts now live in `dtf_local.operators`, one per person, with `movements.operator`
and `order_files.created_by` recording who actually acted. Administered from the API
container with `python3 -m app.operators` (list, add, password, disable, enable);
passwords are read from the terminal so they never reach shell history or the
process list, and disabling revokes open sessions immediately rather than leaving
them valid for the rest of the eight-hour window.
Migration was the risk, since getting it wrong locks the factory out of the Kanban.
`OPERATOR_EMAIL`/`OPERATOR_PASSWORD` seed the first account, once: a password
changed through the CLI is never reverted by a stale environment variable on the
next deploy. The first attempt did not work — `db-init` was not given those
variables in either compose file, so no account would have been created and login
would have failed closed with 503. Both files now pass them to the migration job.
Verified end to end: the unchanged credential still logs in, a second operator
authenticates separately, wrong passwords and unknown accounts are rejected, and
disabling ends access at once.
**Roles are deliberately not included.** The meeting described separation of duties
for rework authorisation (Mayana classifies, Thales or Alexandre authorise), but the
rework feature does not exist in this system, so there is nothing for a role to
gate. Building an authorisation model with no consumer would be guesswork. Add roles
with the feature that needs them.
### `[~]` 2.9 — TLS is terminated outside the repository `(F13)`
Downgraded 2026-09-21. The stack publishes plain HTTP on 18080/18081 while
`COOKIE_SECURE: "true"`, and nothing in the repo provisions certificates — but
`nginx-proxy-manager` on the host owns 80/443 and terminates TLS in front of it,
so cookies are not being dropped in practice. This is undocumented operational
knowledge rather than a live defect.
What remains: record the proxy in `PORTAINER.md` as part of the deployment
contract, so nobody moves the stack to a host without one and silently breaks
every session cookie. `TAREFAS.md` A2 still lists the certificate as pending;
confirm it is actually issued for the DTF subdomain.
### `[ ]` 2.10 — No email verification, no password recovery `(F14)`
A locked-out customer has no path back, and registration accepts any CNPJ without
proving control of the e-mail. Needs a transactional mail provider — **client input**.
### `[ ]` 2.11 — LGPD `(F15)`
CNPJ, phone and e-mail are kept indefinitely in `accounts.profile` and
`orders.snapshot`. Artwork has a 30-day policy; personal data has none, and there is
no privacy notice, consent record or deletion path.
---
## Block 3 · Architecture — needs a decision before code
### `[?]` 3.1 — Manual quote approval contradicts the 24h business case `(F16)`
Payment requires `quotes.approved`, set only by an authenticated operator. The
meeting's premise was that the 17h30 order waiting until 5am is what costs the
money. As built, a 2am order still waits for a person. `CONTEXT.md` frames this as a
temporary development trust boundary — the risk is that it silently becomes the
delivered model.
**Decide:** what makes a quote auto-approvable (mode, metre range, grade floor,
returning customer), and what still routes to a human.
### `[?]` 3.2 — Billable metres are computed in the customer's browser `(F17, F18)`
For loose artwork, `metros` comes from `desenhaMontagem`/`encaixar` — a canvas
alpha-mask packer running client-side. The server never recomputes it.
`passoDe()`/`CELULAS_MAX` coarsen the grid for large sheets and image decoding
differs by browser, so **the same cart can price differently on different devices**.
The code comments reference "o motor do servidor"; that engine does not exist.
Worse, the layout the customer is quoted on is never produced — operators upload
final files by hand, so billed metres ≠ printed metres and a designer redoes work
the site already did.
**Decide:** port the packer to the server as the pricing authority and the print-file
generator, with the browser as preview only. This is the single largest gap between
what was promised in the meeting and what exists.
### `[?]` 3.3 — The 5 GB problem is unsolved `(F19)`
Transport accepts 5 GiB; `SCAN_MAX_BYTES` / ClamAV `StreamMaxLength` release only
≤ 128 MiB. Files above that are quarantined permanently with no path forward. This
is exactly the risk Jorge raised in the meeting.
**Decide:** raise the scan ceiling with a resource/timeout design, or define an
explicit large-file path (staged scan, sampled scan, operator override with audit).
### `[ ]` 3.4 — Upload throughput `(F20)`
8 MiB parts, strictly sequential in `local/static/upload.js:21`, one presign
round-trip per part → ~640 sequential API calls for a 5 GB file, through an nginx
`limit_req` of 20r/s. Add parallelism (4–6 in flight) and batch presigning.
### `[ ]` 3.5 — Payment ordering `(F27)`
`dev_paid` charges before persisting the order and passes no idempotency key.
Harmless with `FakePayment`; with Mercado Pago that ordering is how you get double
charges. Fix as part of 1.1.
---
## Block 4 · Scale and performance
- `[x]` 4.1 — Ten indexes added, each matched to a query the application issues,
and no more: every extra index is paid for on each write. The outbox and live
uploads use partial indexes so they stay the size of the backlog rather than of
all history. Verified against the running database — the planner chooses
`outbox_pending` and `orders_owner` for the queries they exist for.
- `[x]` 4.2 — The board returns every order still in progress, however old, plus a
window of recent finished ones (`BOARD_FINISHED_LIMIT`, default 50) and the true
finished total. An operator can never lose a card they could act on; only terminal
ones are trimmed. The Kanban column reads "Finalizado · 50 de 213" when truncated,
so the count is not mistaken for an all-time total. Pending quotes are capped too.
- `[ ]` 4.3 — Scan throughput `(F21)`: one `scan_loop` thread, `worker` at
`replicas: 1`, ClamAV `MaxThreads 2`, browser gives up after 150s.
- `[ ]` 4.4 — Quality grade fallback `(F22)`: when `carregarImagem` fails,
`px(f)=Math.sqrt(f.size/1024)*95` stands — a DPI inferred from **file size in
bytes** — and it drives up to a 25% discount. Fail closed instead.
- `[x]` 4.5 — Dead config `(F28)`: resolved by deleting `deploy/stack.yaml` in 2.12.
`CLAMD_HOST` no longer appears anywhere; `scanning.py` reaching `'scanner'`
directly is now simply how it works, not a contradiction.
---
## Block 5 · Hygiene and maintenance
- `[x]` 5.1 — CI coverage `(F33)`. An `integration` job now builds the localhost
stack and runs smoke, workflow, security, scanning, retention, runtime security
and both browser suites; `publish-and-deploy` depends on it. Verified by
reintroducing the 0.1 defect: `py_compile` and the unit tests still passed while
`smoke_test` failed on `/session`, blocking the release. Browser tests skip with
a warning when the runner has no Chrome — **install `google-chrome-stable` on the
runner (or set `CHROME_BIN`) to make them gate as well.**
- `[x]` 5.2 — `portal/`, `kanban/`, `agente/`, the root `schema.sql` and
`.env.exemplo` removed: 2,283 lines implementing a model this system abandoned,
referenced by nothing, with several endpoints taking the acting user from the
request body. The documents describing them are archived under `docs/historico/`
with a header saying they are background, not instructions.
- `[x]` 5.3 — Root `requirements.txt` deleted. It pinned by wildcard, listed
packages the system does not use, and sat next to the hash-locked
`infra/requirements.lock` inviting the wrong one to be installed. Only historical
documentation referred to it.
- `[x]` 5.4 — `pip` removed from `infra/requirements.txt` and from the lock. Nothing
depended on it; it was pinned only because it was listed directly, and installing
it put a package manager inside the read-only runtime image. The base image's own
pip performs the hash-enforced install. Verified: the image builds under
`--require-hashes` and reports the base pip, 25.0.1.
- `[ ]` 5.5 — Doc drift `(F34)`. `README.md`, `CONTEXT.md`, `LOCAL_SETUP.md` and
`SECURITY_REPORT.md` describe a MinIO localhost stack, an API with "no external
network route", a `operator` / `local-operator-only` login the email-validated
model rejects, and a release gate — none match the current tree.
- `[x]` 5.6 — The Site's 1,575-line inline script is now nine files under
`local/static/`, cut at the author's own section boundaries so no function was
split: config, modes, upload, sheet analysis, PDF, quality, packing, cart, flow.
`dtf-site.html` is 1,394 lines of markup and style. The extraction was verified
byte-identical before the tags were swapped in, and they load as classic scripts
in the original order, so evaluation semantics are unchanged.
A consequence worth having: with no inline script anywhere, the policy needs no
hash allowlist and is now simply `script-src 'self'`. `site-packing.js` is also
where a server-side packer (3.2) has to agree, which was the point of splitting.
- `[ ]` 5.7 — Commercial rules duplicated between `FAIXAS` (JS) and `TIERS` (Python)
`(F26)`. `test_pricing` guards parity; generate one from the other instead.
- `[ ]` 5.11 — Nothing tests the schema against an empty database. The 4.1 indexes
were added next to the existing one, which sits before the tables they name, so
`CREATE INDEX ... ON dtf_local.order_files` ran before that table existed. Every
local run passed because the volume already had the tables; CI caught it on its
clean volume. A migration is only really exercised from nothing, so the
integration job should run `down -v` before `up` — or a dedicated step should
apply `schema.sql` twice to a fresh database, proving both a first install and
a re-run.
- `[ ]` 5.10 — The browser suites do not run in CI. Chrome runs in the runner
container and can only reach the stack through ports published on the host, which
is a different network namespace when the runner is itself a container. The API,
workflow, security, scanning, retention and runtime suites were moved inside the
stack's network and do gate. The browser suites are the only coverage for the
artwork editor and the full customer journey, so they need either Chrome in a
container on that network, or a runner with host networking. Until then they gate
locally only, and CI warns when it skips them.
- `[ ]` 5.9 — `local/browser_test.mjs` failed once and passed on an immediate
re-run, with no code change in between (2026-09-21). It is a deploy gate when the
runner has Chrome, so an intermittent failure there blocks releases for no reason.
Suspect Chrome startup timing or a race against stack readiness. Watch it, and if
it recurs add an explicit readiness wait rather than a retry.
- `[x]` 5.8 — The Site claimed 90-day file storage and 12-month history in two
places, and invited customers to reorder "sem subir de novo". Files are kept 30
days. The copy now states 30 days, says a later order needs the file again, and
keeps only the true part: order history remains in the account. Policy unchanged;
the promise was corrected to match it.
---
## Done
### Week 1 — infrastructure, uploads, service base
- `[x]` Compose stack: Site, Kanban, API, PostgreSQL, worker, ClamAV, R2/MinIO storage.
- `[x]` Gitea + Portainer publication path; web service startup and API rollout fixed.
- `[x]` Database passwords with special characters handled via discrete libpq fields.
- `[x]` Kanban e-mail/password login; missing operator config no longer breaks stack boot.
- `[x]` Direct resumable multipart browser → private object storage.
- `[x]` Quarantine + ClamAV gate: only `clean` files can be quoted, paid, downloaded or queued.
- `[x]` Retention worker: incomplete 1d, rejected 3d, originals 7d after approval, finals 30d.
- `[x]` Server-side pricing authority with parity test against the Site JavaScript (4,444 cases).
- `[x]` Loose-artwork packing flow: per-file width, copies, rotate, mirror, live 57 cm preview,
5 mm gap, ruler and watermark preserved; metres follow packed height.
- `[x]` Rotation/mirror applied to packing masks; stale renders no longer overwrite a newer preview.
- `[x]` Hash-locked Python dependencies; Trivy reports in `output/security/`.
### Block 0 — 2026-09-18
- `[x]` `import os` restored in `local/auth.py`; `COOKIE_SECURE` is now a single
module constant shared with `local/app.py`.
- `[x]` Navigation links no longer preselect a product (`data-modo-cta` removed).
- `[x]` Ready sheet vs loose artwork is an explicit, priced, reversible selector
(`#tipoEnvio`), shown in all four modes and locked once a file is attached.
The product is the declaration; `sel()` no longer switches anything silently.
- `[x]` `medirFolha` returns `dpiFolha`; an image that cannot span the film width
at `DPI_RECUSA` is refused as a sheet, with one click to send it as loose artwork.
- `[x]` `pintaCaminhos` scoped to `#caminhos .cam` — its global `.cam` selector was
clobbering the new control.
- `[x]` `compose.local.yaml` restored (MinIO, fake providers, builds from source).
- `[x]` `APP_DB_PASSWORD` separated from `POSTGRES_PASSWORD`, with a `bootstrap.py`
guard that refuses identical credentials in both configuration forms.
- `[x]` Checkout confirmation moved out of the panel the success path hides.
### Block 2 and CI — 2026-09-18
- `[x]` The web gateway overwrites `X-Forwarded-For` with the peer address instead
of appending to it, and `client_ip()` resolves the requester for rate-limit
buckets and security events. Verified: a forged `203.0.113.99` never reaches the
audit trail.
- `[x]` Guest sessions are limited per source. The first attempt used 30/IP, which
the new regression caught as too tight for shared NAT — recreating the original
fault in a narrower form — so the ceiling is 240 per 15 minutes, overridable with
`GUEST_SESSION_LIMIT`.
- `[x]` Security events now carry the source address (`operator_login_failed`,
`customer_login_failed`, `cross_origin_rejected`, `http_security_event`).
- `[x]` CI runs the integration suites against a real stack before publishing.
### 2026-09-21
- `[x]` 2.3 — `local/secrets.py` resolves every `<NAME>_FILE` into `<NAME>` from the
API, worker and bootstrap entrypoints, failing closed on an unreadable or empty
secret and on a value supplied both ways. Verified by booting the API, the worker
and bootstrap with credentials supplied only as mounted files, including a
password containing `:/?#[]&=+$ ,%`. `OPERATOR_USER` in the stack became
`OPERATOR_EMAIL`, which is what the runtime reads.
- `[x]` 2.5 — The gate now loads `local/secrets.py` and makes it resolve every
secret `deploy/stack.yaml` declares, plus asserts it fails closed. Verified
against a no-op loader (11 blockers) and one that swallows a missing file
(1 blocker); only the real implementation passes. The four marker strings that
stopped matching when R2 support landed were removed; the two describing real
blockers stay, so the gate still refuses a release while the payment and
messaging adapters are fake.
- `[x]` 2.4 — A blocking Trivy secret scan was added and verified both ways: a
planted AWS key pair, GitHub token and private key block the job; the repository
passes clean. Worth knowing: Trivy allowlists documented example credentials, so
my first probe passed with AWS's own sample keys — the gate is a backstop, not
permission to commit secrets. The source preflight now runs and always prints its
verdict, enforcing only when `ENFORCE_PRODUCTION_PREFLIGHT` is `true`; enforcing
it today would block every deploy, since it refuses a release while the adapters
are fake. Image vulnerabilities are reported after each build, not enforced —
56 HIGH and 3 CRITICAL, only 15 with an upstream fix. `PORTAINER.md` and
`SECURITY_REPORT.md` now carry a table of what gates and what does not, replacing
descriptions of checks that never ran.
- `[x]` 2.6 — Both bases pinned by digest, OS packages upgraded in the production
images, and the web image moved off the nginx 1.28 line.
| Image | Before | After |
|---|---|---|
| API | 56 HIGH, 3 CRITICAL (15 fixable) | 46 HIGH, 0 CRITICAL |
| Web | 5 HIGH, all unfixable in place | 0 HIGH, 0 CRITICAL |
The 1.28 nginx pins `nginx=1.28.3-r1` in `/etc/apk/world`, so `apk upgrade`
cannot patch it even though Alpine ships `-r7`; `nginx:alpine` (1.31.6) is clean
while `1.29-alpine` scans worse at 37 HIGH. The two remaining "fixable" API
findings are `msgpack` and `setuptools`, which I confirmed are absent from the
built image rather than trusting the earlier report. Local images now share the
pinned bases, so the integration suite exercises what ships; full suite passes on
nginx 1.31.6. With both images at zero CRITICAL, the image scan now **gates on
CRITICAL** and reports HIGH.
### 2026-09-21 — from the runner host inventory
- `[x]` Fixed a regression in 2.1: the production gateway sits behind
`nginx-proxy-manager`, so `$remote_addr` there is the proxy, not the customer.
Overwriting `X-Forwarded-For` with it would have recorded the proxy's address for
every request in production — the same bug 2.1 set out to fix. The gateway now
uses `real_ip` to recover the customer's address from the proxy's header, trusting
only private networks, so a request arriving directly at the published port
cannot spoof it. Validated with `nginx -t` against the rendered config.
- `[~]` 2.9 downgraded: TLS is terminated by that proxy, not missing.
### Reporting
- `[x]` Week-1 client report (`Relatorio-Semana-1-DTF.docx`), corrected 2026-09-18 to
remove the inaccurate "Arquivo por metro permanece separado, com seleção explícita"
claim and the internal commit reference.

182
docs/SECURITY_REPORT.md Normal file
View File

@@ -0,0 +1,182 @@
# Local security closeout
Date: 2026-09-15
## Scope and conclusion
This report covers the localhost-only Site, Portal/API, Kanban, PostgreSQL,
MinIO, ClamAV, and fake integration worker. It does not approve production use
or real Mercado Pago, R2, Tiny/Olist, WhatsApp, freight, or other providers.
Malware scanning is a quarantine control; it is not artwork or print pre-flight.
The local stack is healthy and its security regressions pass. Existing orders
and the named PostgreSQL/MinIO volumes were preserved. Open dependency and image
findings remain and are recorded below; there is no claim of zero vulnerabilities,
complete OWASP compliance, or production readiness.
## Implemented controls
- Loopback-only published ports, local-only adapter guards, internal API/worker/
database/scanner network, Host checks, cross-origin write rejection, CSP,
frame denial, MIME sniffing protection, and no interactive API documentation.
- Escaped untrusted filenames and browser regression coverage with CSP bypassed.
- Expiring, revocable, hashed HttpOnly operator sessions. No Basic credentials or
operator password are retained in browser storage.
- Seven-day revocable customer sessions, stronger scrypt password hashes, legacy
verification and upgrade, comparable missing-account password work, and login
throttling.
- Upload extension/size/count quotas, storage quotas, private UUID object keys,
exact signed multipart `Content-Length`, completion/ownership checks, and
five-minute signed downloads.
- Files start `pending`. Only `clean` files may be quoted, approved, paid,
downloaded, attached as final files, or moved into queue/printing states.
Unknown, scanner error, unsafe/encrypted, over-limit, and malware outcomes fail
closed. Rejected/error files expire within three days.
- Restricted PostgreSQL and MinIO runtime identities provisioned separately from
administrator credentials. Containers use read-only filesystems/capability
drops where compatible.
- Structured redacted security logs plus a 30-day PostgreSQL event table. Worker
health includes outbox progress, scan-thread liveness, and a live ClamAV PING.
The alert summary also reports signature age.
- Logout revokes server sessions and clears checkout keys and IndexedDB File blobs
across open Site tabs.
- Production delivery definitions use unprivileged application/web users,
read-only filesystems, dropped capabilities, external Swarm secrets, private
service networking, immutable base images, commit-SHA rollback tags,
health-monitored rolling updates, and automatic application rollback. A
fail-closed source/configuration gate prevents the current local-only runtime
from being released as production.
## Verified checks
All results below were observed against the final application source. Smoke and
workflow were repeated after the refreshed PostgreSQL 17 image was activated.
| Check | Result |
|---|---|
| Pricing parity against Site JavaScript | Pass: all 4,444 comparisons plus invalid inputs |
| Security regression | Pass: headers, Host/origin, sessions, quotas, multipart signing, throttling |
| Real ClamAV EICAR regression | Pass: rejected and blocked from download/quote; clean control released |
| Smoke workflow | Pass: multipart, ownership, all modes, pricing/freight, payment idempotency, states/history |
| Customer/final-file workflow | Pass: identity, revocation, corrections, secure downloads, invalidation and queue gate |
| Runtime security | Pass: PostgreSQL/MinIO least privilege, hashes, quarantine and scanner failure behavior |
| Retention | Pass: expired bytes removed, live bytes preserved, metadata retained |
| Browser workflow | Pass: end-to-end flow, filename XSS probe, no stored operator password, logout blob cleanup |
| Dependency lock | Pass: 26 exact packages, SHA-256 hashes on every entry, direct-pin parity, hash-enforced image build, `pip check` |
| Staging readiness gate | Pass with synthetic non-secret metadata in a network-disabled container; unsafe/incomplete regression cases rejected |
| Production delivery definitions | Pass: unit/syntax/YAML checks, Swarm render, immutable-base image builds, non-root read-only web runtime and Host rejection |
| Production source preflight | Expected block: local-only/fake adapters and Docker secret-file loading are not production implementations |
| Production ClamAV runtime | Pass: UID 100:101, read-only filesystem, no capabilities/no-new-privileges, live PONG |
| Repository secret scan | Pass: no HIGH/CRITICAL Trivy secret findings; release workflow enforces the same gate |
## Malware-scanner operations
The scanner image is digest-pinned and has no external network route. It uses
the signatures bundled into that image. On closeout it reported ClamAV
`1.5.4/28122/Sun Sep 13 06:26:25 2026`; the summary calculated 31.8 hours of age,
below the seven-day alert threshold.
The multipart transport supports uploads up to 5 GiB, but `SCAN_MAX_BYTES` and
ClamAV stream limits release at most 128 MiB by default. Larger files remain
blocked. Supporting larger files requires a deliberate resource/timeout design,
not simply increasing the upload limit.
Run:
```bash
docker compose exec -T api python3 -m app.security_status
```
An exit status of 1 requires review. At closeout, attention was expected because
the regression suite produced two rate-limit alerts, 20 synthetic failed operator
logins, and two rejected `SECURITY-EICAR.cdr` fixtures. Both rejected fixtures
expire on 2026-09-17. ClamAV was available and its signatures were not stale.
Do not automatically dismiss later alerts: verify filename, timestamp, test run,
scanner availability, and signature age. Treat non-test rejected files, scanner
errors, unexplained authentication bursts, or stale signatures as incidents.
## Dependency and image audit
`pip-audit` inspected the exact packages installed in the hash-enforced rebuilt API
and found no known Python advisories on 2026-09-15. All 26 direct and transitive
runtime packages are pinned with artifact hashes in `infra/requirements.lock`.
`local/lock_dependencies.sh` regenerates it in a disposable Python 3.12 container.
This is a point-in-time package-database result, not proof that the dependencies
or image are vulnerability-free. Scheduled lock refresh and audit automation remain
unfinished.
Trivy JSON reports are in `output/security/`. HIGH/CRITICAL results after the
available custom-image package upgrades were:
| Image | Findings | Qualification |
|---|---:|---|
| API | 46 HIGH | 44 Debian records had no fix; Trivy's two Python records named `msgpack` and `setuptools`, which `pip list` confirmed are absent from the runtime. Trivy warned that the third-party SBOM may be inaccurate. |
| Site | 5 HIGH | Nginx package records with fixes listed by Trivy, but the current official `nginx:1.28-alpine` image/repository did not supply them. |
| PostgreSQL 17 | 30 HIGH, 1 CRITICAL | Nine Alpine library records and 22 records in `/usr/local/bin/gosu`; presence is confirmed, reachability through this local stack is not. |
| MinIO | 102 HIGH, 6 CRITICAL | Findings span the MinIO and `mc` binaries and two OS packages. A verified local clean-object archive now exists, but the old pinned release was not changed without a tested version-migration and rollback plan. |
| ClamAV | 0 HIGH/CRITICAL | This scan result is not a guarantee that no vulnerability exists. |
| Production API validation image | 55 HIGH, 3 CRITICAL | 44 records had no fix. Fourteen listed fixes, but the `msgpack` and `setuptools` records came from a third-party SBOM and both packages were confirmed absent with `pip list`; the remaining fixed records are Debian packages. |
| Production web validation image | 52 HIGH, 2 CRITICAL | All 54 records listed fixed Alpine versions. A current approved Nginx base digest must replace the locally available validation base before release. |
Scanner presence is evidence, while exploitability/reachability requires separate
analysis. MinIO and the remaining base-image findings block any production-readiness
claim even though ports are loopback-only here. The production validation reports
are `production-api-container-audit.json` and
`production-web-container-audit.json`; both make the release workflow's
HIGH/CRITICAL gate fail. Counts reflect the 2026-09-15 Trivy database and can
change when the advisory database or selected base digest changes.
## Production delivery security boundary
The files in `deploy/` and `.gitea/workflows/` are a guarded delivery mechanism,
not an approval to operate the current application on the public internet.
Corrected 2026-09-21: an earlier version of this section described gates the
workflow did not contain. The workflow now runs static validation, the
integration suite against a live stack, and a blocking Trivy secret scan before
publishing. The source preflight is advisory unless
`ENFORCE_PRODUCTION_PREFLIGHT` is set, and image vulnerabilities are reported
rather than enforced, because the current bases carry HIGH/CRITICAL findings
with no upstream fix. Base images are still mutable tags, not digests.
`PORTAINER.md` holds the authoritative table of what gates and what does not.
It publishes both `latest` and the full commit SHA, then calls the Portainer
webhook. Application/provider secrets are created directly as versioned external
Swarm secrets and never cross the workflow. Rollback selects the prior commit SHA
in Portainer and does not roll back the database.
The gate currently identifies real blockers: production adapters and authenticated
payment webhooks are absent, the runtime intentionally rejects production/R2,
Docker secret-file configuration is not implemented, approvals are unset, and
the existing image findings are unresolved. These checks must be satisfied by
implementation and review, not by replacing the checks with permissive values.
## PDF.js review
The Site loads version 3.11.174 from a versioned CDN URL. That version is affected
by [GHSA-wgrm-67xf-hhpq](https://github.com/mozilla/pdf.js/security/advisories/GHSA-wgrm-67xf-hhpq),
whose documented workaround is `isEvalSupported:false`; the existing Site call
already sets that value, so the known eval path is mitigated and is not reported
as unmitigated. The newer
[GHSA-hq66-cqwq-w95j](https://github.com/mozilla/pdf.js/security/advisories/GHSA-hq66-cqwq-w95j)
affects versions from 5.6.83 up to the patched 6.2.108 and therefore does not
include 3.11.174.
Version 3.11.174 is nevertheless old, remotely loaded, and not a satisfactory
long-term dependency posture. Plan a compatibility-tested upgrade and preferably
vendor or integrity-pin the asset. Keep script execution disabled and do not
weaken CSP merely to support a preview.
## Before any staging or production work
- Resolve or formally accept current MinIO, PostgreSQL, Nginx, and Debian image
findings. Use the verified local clean-object bundle to rehearse a MinIO
migration and rollback; it is not a scheduled, offsite, or production backup.
- Schedule controlled dependency-lock refreshes and exact-runtime audits; review
and test every resulting version change.
- Establish a signature update/rebuild process that works without giving the
scanner an unrestricted external network route.
- Replace disposable local secrets; add TLS, production session/cookie settings,
account verification/recovery, centralized monitoring, and complete backup/
restore procedures.
- Re-run every security, malware, workflow, browser, retention, dependency, and
image check in the target staging architecture.