Files
dtf-system/docs/ROADMAP.md
Cauê Faleiros 988b252f9d
All checks were successful
Build and deploy / Validate source (push) Successful in 11s
Build and deploy / Integration suite on a real stack (push) Successful in 2m39s
Build and deploy / Secret scan and release gate (push) Successful in 7s
Build and deploy / Publish images (push) Successful in 1m43s
feat: check Tiny products and add a supervised order test
"Testar conexão" now also reads the four configured Tiny products and
requires each to be active. app/tiny_probe.py runs from the worker console
to list products, confirm the configured ids, and create one marked test
order through the worker's own delivery path, proving the duplicate guard
by search before a second delivery. Nothing is sent without --confirmar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:23:07 -03:00

52 KiB
Raw Blame History

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 (2026-09-25, last day of Week 2): Still waiting on the client for Mercado Pago credentials and webhook access (1.1) and freight data (1.2); Tiny's app credentials are in production and the connection starts from the Kanban (1.3). Built and deployed without them: server-side print-file generation (1.4), the delivery address (3.8), the Kanban's payment-issue and print-file views and its redesign (1.5), the Site redesign (5.17), and Mercado Pago and Tiny adapters written from the public API documentation and tested against fake transports. None of the provider work is a verified integration. Every change passed the full integration sequence locally (Docker Engine in WSL) and on the Gitea runner, which publishes images on green pushes to main; the user verified each release in production after redeploying in Portainer. Operator and Site guides are in docs/ (see Reporting). Left for today: product ids for the four products from the client's Tiny and one supervised real order (1.3); a Mercado Pago sandbox payment by PIX and card if the credentials arrive (1.1); the freight connector if the data arrives (1.2, otherwise the one Week 2 item that slips, on client inputs); the Week 2 client report.

Previous step (2026-09-23): Payment safety fixes 2.13 and 2.14 and the local order-correctness work in 3.6/3.9 have passed integration checks. Production specification v2 now records each copy's film coordinates and is kept through the approved order; 4.6 now pages pending and approved unpaid quotes, including a tested 101st pending quote. Operational entrypoints in 5.12 are repaired and locally exercised. Image decoding, mixed-sheet grading, rotation-sensitive DPI, and PDF page geometry are corrected in 3.9/4.4. main pushes now validate without publishing; manual release requires a passing source preflight. The containerized browser gate passes locally, pending a Gitea runner run. Next address the upload/scanner safety gate and unsupported PDF image evidence. The customer/API upload admission now stops above the scanner's effective limit before transfer; the 5 GiB large-file product path still needs agreement and implementation. Unfinished upload reservations now expire after one hour or can be cancelled explicitly; anonymous admission and browser resource bounds stay open. Generated print output, lifecycle/recovery, and provider work remain open. Obtain decisions for unattended pricing, print-file acceptance and large files, plus sandbox inputs for freight and Mercado Pago. Week 2 delivery items 1.1–1.5 remain open; 1.6 is complete.

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 the then-current tree. The 2026-09-21 full review is docs/REVIEW-2026-09-21.md; the 2026-09-22 review and payment probes added new findings to Blocks 2–5 below. Historical paths in closed items remain as recorded.

Full remediation register: docs/REMEDIATION-2026-09-22.md maps every one of the 37 review findings to an action and a release gate. Use it alongside this Week 2 tracker; a green milestone here does not close the production gate.

Status Meaning
[ ] not started
[~] in progress
[x] done and verified
[?] blocked on a decision (product or client), not on code

Week 2 execution sequence

This sequence keeps the client commitments in Block 1 visible while correcting defects that would make those commitments unsafe or impossible to operate. Do not mark a provider item complete from a fake-adapter test or a healthy page.

Order Work Exit evidence
1. Immediate safety — done 2026-09-22 Close 2.13 and 2.14; cover foreign quote IDs, missing/invalid amounts, duplicates and valid approvals. Local integration checks pass and no other customer's order is returned.
2. Order correctness Fix 3.6 and 4.6: one current cart/quote snapshot, versioned per-file production instructions, correction/final revision binding, visible actionable quotes. The approved quote, order and final file can be traced back to the same reviewed layout; edits cannot buy an old cart.
3. Resolve product contracts Decide 3.1–3.3: which quotes may auto-approve, what generates the print file, and which sizes the upload and scanner can release. Written acceptance rules and representative artwork/large-file cases before enabling unattended payment.
4. Week 2 integrations Add destination data and real freight first, then Mercado Pago payment intents/webhooks/reconciliation, then Tiny/Olist order creation. Keep the four agreed WhatsApp events in the same delivery contract. Sandbox flows and failure/retry cases pass; no fake provider is presented as production ready.
5. Operability and release Repair 5.12–5.14, signatures, proxy trust and backup/restore; gate browser tests and the exact deployed images. Fresh install, upgrade, recovery and deployed release checks pass with alert ownership recorded.

Client inputs needed for steps 3–4 are listed in docs/PRODUCTION_INPUTS.md. Engineering can complete steps 1–2 and repair local operational commands while those inputs are gathered. The full disposition of architecture, security, quality, operational, and delivery findings is in docs/REMEDIATION-2026-09-22.md; all release gates there must be met before accepting real customer work.


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.

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. Current foundation (2026-09-22): a fake signer exercises signature rejection, event-ID deduplication, amount comparison and transactional order creation. This is not a Mercado Pago integration. Complete a durable payment intent, provider payment ID and currency binding, real verification and status lookup, delayed/duplicate event handling, refund/cancellation rules and reconciliation. A refused paid event must be visible for operator resolution rather than silently treated as finished. See 2.14 and 3.7. Requires sandbox access, webhook administration, event mapping and an approved refund policy. Groundwork (2026-09-24): app/mercadopago.py creates PIX or card-token payments with the quote as idempotency key, verifies x-signature as documented (HMAC-SHA256 over id;request-id;ts, 30-minute replay window), and treats the notification as a pointer: the payment is fetched from the API and only a BRL amount in whole centavos is compared. payment_intents binds each provider payment to its quote; /api/payments/intent starts a PIX and the Site shows its QR code. Refused paid events and refunds on existing orders now stay on the Kanban until an operator records a resolution. Unit-tested against a fake transport only. Card form (2026-09-24): Mercado Pago's Card Payment Brick on the Site, shown when MP_PUBLIC_KEY is set; the card becomes a one-time token in Mercado Pago's secure fields. Each card attempt has its own idempotency key (a decline can be retried with another card) and the intent route refuses any new attempt once a payment is approved or a card is in review, so a quote cannot be charged twice. The Site CSP gains the Mercado Pago origins only through PAYMENT_CSP_SOURCES, empty by default. Not yet rendered against a real public key; sandbox run and refund policy remain.
  • [ ] 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. Groundwork (2026-09-24), API v3 by decision: OAuth2 against Tiny's Keycloak. An operator starts the connection from the Kanban; the callback is authorised by a single-use state (the operator cookie is SameSite=Strict and does not survive Tiny's cross-site redirect). Tokens live in provider_tokens; the refresh token rotates under a row lock and the worker keeps the connection alive. Orders: contact found by CNPJ or created, then POST /pedidos with product ids from TINY_PRODUCT_TEXTIL_FOLHA / _TEXTIL_AVULSA / _UV_FOLHA / _UV_AVULSA (not _<MODE>: a name ending in _FILE is read as a secret file path by app/core/secrets.py) and numeroOrdemCompra = DTF-<number>; a retry searches the customer's last seven days of orders for that number first. Production passes the app credentials through but keeps TINY_ADAPTER: fake. Tested against fake transports (tests.test_tiny) and, for OAuth, the real database (tests.tiny_oauth_test). Tiny has no sandbox: the first real test creates real orders. Still to confirm on the client's account: plan (Construa+), product ids, token lifetimes, whether pickup needs a transportador, and rate limits. Supervised run (2026-09-25): the payload and query parameters were checked against Tiny's published v3 OpenAPI spec (GET /pedidos accepts cpfCnpj and dataInicial; GET /pedidos/{id} returns numeroOrdemCompra). "Testar conexão" on the Kanban now also reads the four configured products and requires each to be active. python -m app.tiny_probe runs from the worker console: produtos and conferir are read-only; pedido shows the test order and creates it only with --confirmar, through the worker's own deliver, then proves the duplicate guard by search first and only then by a second delivery. Not yet run against the client's account.
  • [~] 1.4 — Final print-file generation (see 3.2 and 3.6: production instructions must survive checkout before an output engine can reproduce the approved job). Built (2026-09-24): each paid item gets a PDF the width of the film and the length of the approved layout, with every copy at its reviewed position, rotation and mirror (app/printfile.py, rendered by the worker from app/printjobs.py). Sources are embedded once at original resolution; JPEG bytes pass through, PNG alpha becomes a soft mask, EXIF orientation is honoured. A file whose proportions differ from the quote, a layout longer than billed, or PDF/PSD/AI/CDR artwork goes to hand preparation with the reason. The operator approves the generated PDF as the final file through the existing review. Unit tests include a raster check of every rotation and mirror, and tests.print_file_test passes on the running stack (generate, download, approve as final, queue; hand-preparation routing and retry). Still open: a FlexiPRINT import of real generated files (including one longer than 5 m, which uses UserUnit). PDF artwork (2026-09-24): a single-page PDF source is placed as a vector form through pikepdf (MPL-2.0; PyMuPDF was rejected for its AGPL licence), using the CropBox and inherited /Rotate the Site measured with pdf.js. Raster tests cover crop, page rotation, placement rotation and mirroring, and were shown to fail when the rotation or crop handling is broken. Multi-page and protected PDFs go to hand preparation.
  • [~] 1.5 — Main Kanban production states consolidated. The six states and their transitions are unchanged; cards now show the delivery address, the print-file status per item, and a panel lists payments that need a person (money without an order, refunds after an order) until resolved. Confirm with the operation that these are the main states before closing. 2026-09-24: a mistaken move can be undone one stage back (BACK in app/runtime.py) with an internal reason, flagged in the history (movements.back), without notifying the customer; "production started" and "ready" are enqueued once per order. Previously the only way back was Correção, which messages the customer and invalidates approved finals. Redesign (2026-09-24): tabs for Produção, Cotações, Pagamentos and Integrações; an order panel with stages, items, print files, final files and history; numbered pagination (20 per page) on quotes, payment issues and the integration log, which also has filters; messages dismiss themselves. The operator guide (docs/guia-operador-kanban.pdf) documents the flow for the operation to confirm.
  • [x] 1.6 — Block 0.2 + 0.3 were completed and verified on 2026-09-18.

[!] The production compose currently blocks dev_paid (ENVIRONMENT != 'local') and ships only fake adapters, so the deployed system cannot take an order at all. Real freight, payment initiation and verified provider events are required to unblock it; the fake webhook alone does not.


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 no defined retention, export or deletion path. The Site links an external privacy notice; confirm that it covers this processing and define the required records and customer rights flow before production activation.

[x] 2.13 — A foreign paid quote ID exposes an order (2026-09-22 review)

The dev-paid refusal fallback fetched orders by quote_id without owner. A separate local customer session received the full paid order when supplied another customer's quote ID. app/api/orders.py now includes the owner in the fallback query. The local payment integration test confirms a foreign ID returns 404 while the owner can still retrieve the already-paid order.

[x] 2.14 — A signed approval without a paid amount creates an order

app/payments.py compared amounts only when the event contained one. A local signed approved event without amount_cents created an order. The service now requires an actual integer amount equal to the approved total; local integration tests cover missing, non-integer, underpaid and correct values. Currency and provider payment identity belong to the wider contract in 3.7; this gate does not complete 1.1.

[~] 2.15 — Public intake controls need operational proof

Unfinished reservations now have a one-hour lease and owner-scoped cancellation; anonymous admission capacity still needs a firm bound. Keep ClamAV signatures current and alert on stale data; scope reverse-proxy IP trust to the actual hop and verify it through Swarm ingress. These are separate controls, but all must work before public large-file intake is considered safe. The production topology has not been verified by the repository review.


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. As of 2026-09-23, customer selection and API reservation reject files above the effective scan limit before transfer, and the Site displays the current limit. This prevents a doomed upload; it does not deliver the promised 5 GiB path. 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)

The local fake pay call now runs inside the order transaction, and the inbound webhook records and applies a delivery transactionally. This does not make an external charge atomic with PostgreSQL: a provider can succeed while the database write fails, or deliver the approval later. Add a durable payment intent, provider idempotency key and reconciliation as part of 1.1 and 3.7.

[~] 3.6 — Preserve and bind the order the customer actually reviewed

The browser's width, copies, rotation, mirroring and repetitions are absent from the API item, so the factory cannot reproduce the priced layout. Removing an artwork can leave a stale cart item; editing after quote creation can leave the old quote payable; a new correction can leave an obsolete final active. Persist a versioned per-file production specification, tie the displayed cart to its immutable quote, and tie final approval to the latest correction revision. Cover the real editor-to-quote-to-final journey, not only the pricing table.

Local progress 2026-09-23: The Site includes per-upload width, length, copies, rotation, mirroring, measurement source, and the exact placement of each copy in production specification v2. The API checks coverage, dimensions, film bounds, and quote height; commercial review cannot replace the layout. The order snapshot and downloadable Kanban manifest retain it. Browser quote actions are disabled when the cart differs, including same-price changes. Editor changes invalidate the current cart item immediately; a new customer correction deactivates prior finals. Browser and local API regressions pass. Still open: generate and validate the final print file from the approved source revision, and make quote/cart continuity work across devices through a server-authoritative confirmation flow. Current quote binding is a browser guard.

[?] 3.7 — Complete payment state and reconciliation rules

Event-ID deduplication does not establish which provider payment settled which quote. Define intent creation, provider transaction ID, currency, paid-at time, pending/rejected/refunded/cancelled states, late approval after quote expiry, overpayment and provider success followed by database failure. Record refused paid events for resolution. Decide who reconciles them and when production must stop or refund. Implement with 1.1 after the checkout/refund policy is approved.

[~] 3.8 — Collect a deliverable destination before charging freight

The quote has a shipping service and CEP but no recipient, street, number, city/state or delivery snapshot. Add and validate these fields with 1.2, then bind the chosen service and final freight amount to the payment intent.

Local progress 2026-09-24: the Site collects recipient, street, number, complement, district, city and UF for delivery; the API requires them for any non-pickup quote, requires the CEP to be the one freight was quoted for, and refuses an address on pickup. The address is part of the reviewed quote, the order snapshot, the Kanban card and the Tiny payload. Changing it after a quote invalidates that quote in the browser like any other cart change. Binding the chosen freight service to the payment intent waits on 1.2.

[~] 3.9 — Make artwork quality and geometry evidence explicit

Reject or route for review when PDF page count/geometry, image decoding or DPI cannot be established. Do not infer pixels from compressed file size, grade a mixed item from only the readable files, silently shrink oversized artwork, or allow a displayed DPI rejection to proceed through checkout. Use representative real artwork in acceptance checks.

Local progress 2026-09-22: DPI refusal and warning acknowledgement now gate the cart and quote API records the acknowledgement. Oversized loose-art width is rejected in the UI, API production contract, and packer. On 2026-09-23, unreadable loose images are blocked, mixed analyzed/manual sheets receive no automatic grade or discount, and rotated DPI uses the pixel dimension that corresponds to printed width. PDF dimensions now come from the parsed page model, with page count, crop, rotation, and UserUnit checks; multi-page and malformed PDFs block quoting. Isolated browser checks cover those cases and same-origin PDF rendering. Unsupported PDF image operators and representative print-file evidence still need correction before this item can close.


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 now have a paginated view; older completed orders still need search in 4.6.
  • [ ] 4.3 — Scan throughput (F21): one scan_loop thread, worker at replicas: 1, ClamAV MaxThreads 2, browser gives up after 150s.
  • [x] 4.4 — Failed image decoding no longer infers pixels from compressed file size. Unreadable loose images cannot enter the cart or receive a grade; an isolated browser regression covers the failure path (2026-09-23).
  • [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.
  • [~] 4.6 — Older unpaid quotes can disappear behind the board limit. On 2026-09-23 the board began showing newest pending and approved unpaid quotes separately, with cursor pagination and counts; a local regression retrieved all 105 pending and 22 approved fixture quotes and cleaned them up. Still open: an explicit terminal state for abandoned/expired quotes and search/history for older completed orders.

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 were moved into a Chrome container on the Compose network and made required in CI. Confirm the complete checkout journey passes in that topology and on the actual Gitea runner before closing this item.

  • [ ] 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.

  • [x] 5.12 — Repair operational entrypoints after the local/ split. On 2026-09-23, staging and both API images package ops/; staging calls ops.staging_readiness; local backup calls ops.storage_backup through compose.local.yaml; documented security and backup commands use the real modules. Verified a network-disabled staging pass with non-secret fixture data, a production API image import, local security status, and a local backup/restore of the database plus 78 clean objects. Production offsite recovery and signature freshness remain separate open items.

  • [x] 5.15 — MinIO stopped publishing public images: by 2026-09-24 both Docker Hub and quay.io answered anonymous pulls with 401, so a runner or machine without a cached image could not start the stack. The local/CI storage and storage-init now use Chainguard's MinIO build (ships sh and mc, non-root), pinned by digest. Verified with a fresh local build and the full integration sequence. Production uses R2 and is unaffected.

  • [x] 5.16 — The operator login limit (10 per account per 15 minutes) counted successful logins too, so ordinary use could lock an operator out, and one extra test login made CI's final browser sign-in fail with 429. Now every attempt counts against the source address and only failures count against the account; registration still counts every attempt. The security suite's lockout check (ten failures, then 429) is unchanged and passes.

  • [x] 5.17 — Site redesign (2026-09-24), Dropstar brand kept; the previous look is tag ui-v1. The home, one page per product (/arquivo-por-metro, /artes-avulsas, /uv-arquivo-por-metro, /uv-artes-avulsas) and /carrinho have their own addresses but stay one document, so artwork held in the browser survives moving between them (web/site-pages.js, web/site-steps.js; nginx serves index.html for those paths). The product page is built around a buy box (web/site-compra.js) that shows the item the flow already priced (grade, price per metre, metres charged, total, resolution acknowledgement and "Adicionar ao carrinho"); the duplicated quality and preview panels are hidden. The cart has "Remover" per item, "Esvaziar carrinho" and an 8-second undo. Fixed on the way, all already in production before: the saved-cart note took a grid column and pushed the order into a narrow strip, the panels outside .w ran edge to edge, and "57 cm" wrapped on the ready-sheet option. The browser suite covers product and cart addresses, reload on a product page, and remove/undo; the security suite checks the new addresses carry the same CSP.

  • [ ] 5.13 — Define production recovery: scheduled encrypted offsite database and object backups, a consistent snapshot boundary, Swarm data placement and a restore rehearsal that opens every required live order file.

  • [ ] 5.14 — Promote and verify one immutable release. 2026-09-24: green pushes to main now publish images and Portainer's pull-and-redeploy is the release gate; the source preflight is advisory unless enforced by variable, because enforcing it while the adapters are fake made every release fail. Previously: normal main pushes ran checks only; manual dispatch required source preflight and a configured webhook, and scans images before publishing. Still make the full preflight validate the active stack, deploy the tested immutable image references, test clean install and upgrade, check application readiness after Portainer redeploys, and isolate concurrent CI stacks.


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] Operator guide (docs/guia-operador-kanban.pdf) and Site guide (docs/guia-site-dtf.pdf), 2026-09-24, with screenshots of a throwaway stack and fictional orders. The payment step has no screenshot until Mercado Pago is configured, and the guides name the provisional Kanban domain; regenerate them when either changes. Early work toward Week 3's "orientação à operação".
  • [ ] Week-2 client report, due end of day 2026-09-25.
  • [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.