Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 KiB
DTF System — Working Roadmap
Internal engineering tracker. Not a client document, not a promise sheet. The client-facing narrative lives in
docs/roadmap-cliente.pdfand 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 is connected in production and reads orders, but the connected user
cannot read contacts (403) and the client's catalogue has no per-metre UV
product; both wait on the client (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: the Week 2 client report. Waiting on the client: the
Contatos permission and the product decision, then product ids and one
supervised real order (1.3); Mercado Pago credentials for the sandbox PIX and
card payments (1.1); freight data (1.2, the one Week 2 item that slips, on
client inputs).
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/becameapp/, withtests/,ops/,infra/andweb/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 ostolocal/auth.py. - Move the
COOKIE_SECUREread to a module constant so it is evaluated once. - Accept: a fresh browser hits the Site and
/api/sessionreturns 200 with acart_scope;local/smoke_test.pyandlocal/workflow_test.pypass.
[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
.camselector — 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}/approvebefore 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.yamlwith 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.mdmatches 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_appwith 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.pycreates PIX or card-token payments with the quote as idempotency key, verifiesx-signatureas documented (HMAC-SHA256 overid;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_intentsbinds each provider payment to its quote;/api/payments/intentstarts 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 whenMP_PUBLIC_KEYis 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 throughPAYMENT_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 (seePRODUCTION_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 inprovider_tokens; the refresh token rotates under a row lock and the worker keeps the connection alive. Orders: contact found by CNPJ or created, thenPOST /pedidoswith product ids fromTINY_PRODUCT_TEXTIL_FOLHA/_TEXTIL_AVULSA/_UV_FOLHA/_UV_AVULSA(not_<MODE>: a name ending in_FILEis read as a secret file path byapp/core/secrets.py) andnumeroOrdemCompra = DTF-<number>; a retry searches the customer's last seven days of orders for that number first. Production passes the app credentials through but keepsTINY_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 /pedidosacceptscpfCnpjanddataInicial;GET /pedidos/{id}returnsnumeroOrdemCompra). "Testar conexão" on the Kanban now also reads the four configured products and requires each to be active.python -m app.tiny_proberuns from the worker console:produtosandconferirare read-only;pedidoshows the test order and creates it only with--confirmar, through the worker's owndeliver, then proves the duplicate guard by search first and only then by a second delivery. Not yet run against the client's account. Staying connected (2026-09-25): Tiny documents a 4-hour access token and a 1-day refresh token; the worker renews about every 4 hours. The connection now asks foroffline_access(listed by Tiny's Keycloak; if refused for this application the callback retries once without it). Renewal failures are stored: a refused refresh token marks the connection lost and is not retried, a transient failure shows as a warning until the next success, and a session grant with under 12 hours left is flagged. Previously a refused refresh still showed "Tiny conectado". Whether Tiny grants offline access, and whether a session grant has an undocumented maximum, is only known on the client's account. There is no alert channel yet (no e-mail; WhatsApp is fake): problems show on the Kanban only. Client account (2026-09-25): connected in production with the developer user the client provided. "Testar conexão":pedidosok,contatos403, unchanged after reconnecting. The application's permissions are correct (Contatos, Pedidos: read and include/edit; Produtos: read); Olist documents that v3 calls also depend on the logged-in user's module permissions, and that user's Cadastros menu shows only Produtos. Orders need Contatos (search by CNPJ, create when new; delete never). Client to decide: grant that user Clientes e Fornecedores, or reconnect with a user that has it (a dedicated integration user is recommended). Products (2026-09-25), from the client's product export: none of the 36 "DTF" products matches the four Site products.951438842"IMPRESSÃO DTF PERSONALIZADO 57X100 (1 METRO)" (MT, R$ 19,90, active) fits Têxtil; there is no per-metre UV product (the UV one is 27 cm, per 10 cm), and no separate "artes avulsas" product. Client to decide: create four per-metre products (recommended, copying951438842), or share951438842between the two Têxtil modes; UV needs a product either way. The item note already carries the Site mode and grade. No product ids are set in production yet; both questions go to the client with the Mercado Pago credentials request.[~]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 fromapp/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, andtests.print_file_testpasses 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 usesUserUnit). 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/Rotatethe 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 (BACKinapp/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-headerswithFORWARDED_ALLOW_IPSscoped to the nginx service, or readX-Forwarded-Forexplicitly 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*_FILEwith env fallback. ReconcileOPERATOR_USERvsOPERATOR_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 choosesoutbox_pendingandorders_ownerfor 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): onescan_loopthread,workeratreplicas: 1, ClamAVMaxThreads 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 deletingdeploy/stack.yamlin 2.12.CLAMD_HOSTno longer appears anywhere;scanning.pyreaching'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). Anintegrationjob now builds the localhost stack and runs smoke, workflow, security, scanning, retention, runtime security and both browser suites;publish-and-deploydepends on it. Verified by reintroducing the 0.1 defect:py_compileand the unit tests still passed whilesmoke_testfailed on/session, blocking the release. Browser tests skip with a warning when the runner has no Chrome — installgoogle-chrome-stableon the runner (or setCHROME_BIN) to make them gate as well. -
[x]5.2 —portal/,kanban/,agente/, the rootschema.sqland.env.exemploremoved: 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 underdocs/historico/with a header saying they are background, not instructions. -
[x]5.3 — Rootrequirements.txtdeleted. It pinned by wildcard, listed packages the system does not use, and sat next to the hash-lockedinfra/requirements.lockinviting the wrong one to be installed. Only historical documentation referred to it. -
[x]5.4 —pipremoved frominfra/requirements.txtand 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-hashesand reports the base pip, 25.0.1. -
[ ]5.5 — Doc drift(F34).README.md,CONTEXT.md,LOCAL_SETUP.mdandSECURITY_REPORT.mddescribe a MinIO localhost stack, an API with "no external network route", aoperator/local-operator-onlylogin 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 underlocal/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.htmlis 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.jsis also where a server-side packer (3.2) has to agree, which was the point of splitting. -
[ ]5.7 — Commercial rules duplicated betweenFAIXAS(JS) andTIERS(Python)(F26).test_pricingguards 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, soCREATE INDEX ... ON dtf_local.order_filesran 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 rundown -vbeforeup— or a dedicated step should applyschema.sqltwice 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.mjsfailed 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 thelocal/split. On 2026-09-23, staging and both API images packageops/; staging callsops.staging_readiness; local backup callsops.storage_backupthroughcompose.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 (shipsshandmc, 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 tagui-v1. The home, one page per product (/arquivo-por-metro,/artes-avulsas,/uv-arquivo-por-metro,/uv-artes-avulsas) and/carrinhohave 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 servesindex.htmlfor 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.wran 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 tomainnow 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: normalmainpushes 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: onlycleanfiles 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 inoutput/security/.
Block 0 — 2026-09-18
[x]import osrestored inlocal/auth.py;COOKIE_SECUREis now a single module constant shared withlocal/app.py.[x]Navigation links no longer preselect a product (data-modo-ctaremoved).[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]medirFolhareturnsdpiFolha; an image that cannot span the film width atDPI_RECUSAis refused as a sheet, with one click to send it as loose artwork.[x]pintaCaminhosscoped to#caminhos .cam— its global.camselector was clobbering the new control.[x]compose.local.yamlrestored (MinIO, fake providers, builds from source).[x]APP_DB_PASSWORDseparated fromPOSTGRES_PASSWORD, with abootstrap.pyguard 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 overwritesX-Forwarded-Forwith the peer address instead of appending to it, andclient_ip()resolves the requester for rate-limit buckets and security events. Verified: a forged203.0.113.99never 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 withGUEST_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.pyresolves every<NAME>_FILEinto<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_USERin the stack becameOPERATOR_EMAIL, which is what the runtime reads. -
[x]2.5 — The gate now loadslocal/secrets.pyand makes it resolve every secretdeploy/stack.yamldeclares, 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 whenENFORCE_PRODUCTION_PREFLIGHTistrue; 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.mdandSECURITY_REPORT.mdnow 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-r1in/etc/apk/world, soapk upgradecannot patch it even though Alpine ships-r7;nginx:alpine(1.31.6) is clean while1.29-alpinescans worse at 37 HIGH. The two remaining "fixable" API findings aremsgpackandsetuptools, 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 behindnginx-proxy-manager, so$remote_addrthere is the proxy, not the customer. OverwritingX-Forwarded-Forwith 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 usesreal_ipto 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 withnginx -tagainst 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.