Picking up @fernandorocagonzalez's question from June, which the roadmap has half answered in practice.
Almost everything we have shipped has been the parts rather than the design: Python 3.14, Django 6, React 19 on PatternFly 6, TypeScript, Vite, Vitest, Ruff, uv, and the ASGI and rebranding work now being planned.
None of that is misplaced: the parts are where the CVEs live and the design is not, which for a security first product is the right order. What it leaves is a refreshed stack on a decade-old design.
What going to a modern design would change
Seven changes, each with something open against it now, though often a first step rather than the change itself:
- Contract first: the schema as the source. Expansion stays on, by decision. #941, #934.
- An async request path: measured, and it is not one. #964, #942.
- A UI decoupled from Django: the SPA served as static files. Done in #952.
- Declarative permissions: the reads take to it, the writes do not. #950.
- Events with retention: a hot window and an archive behind it. Done in #949.
- One process per container: logs on stdout, no supervisor tree. Step one in #942.
- Typed configuration: loaded and validated once. One clause of it in #947.
Declarative permissions: the reads, not the writes
Declarative permissions turned out to be half an item:
- The reads are one line each and nearly all the same line, so #950 states nineteen as data.
- The writes are not a table: 82% branch on the change proposed, or answer with a reason.
The async request path is not one
The third premise here to fail on measurement, and the one I was most confident about. The same twenty row read written def and async def, served by uvicorn:
- One request at a time: the async one is a fifth slower.
- Under load: level with it.
- Why: the ORM is synchronous, so the view still crosses into a thread. The hop moves, not goes.
- Django refuses it anyway: "You cannot use ATOMIC_REQUESTS with async views".
So #942 is one server instead of two, not a down payment on a later win.
summary_fields keeps its shape
Settled rather than open: compatibility with what callers already read beat the bytes.
- #941 is the whole of it: asking for less measured 26% smaller and a fifth quicker on fifty items.
- What is left of contract first is the schema as the source: #934 types the UI and fails on drift.
- ascender-kit has no generated client yet.
The eighth, which measurement took off the list
There was an eighth, a flat job model with a table per job type. #961 measured what the current shape costs: nothing worth a rewrite, so it is off the list rather than waiting on it.
What the expensive two would buy
The first two are rewrites, so they need a payoff beyond tidiness:
- The API: smaller list responses, and generated clients for the UI and ascender-kit.
- The request path: less than this list claimed, which I only found by going to measure it.
What actually holds a worker
- Live job output holds none: a websocket on daphne, and eight viewers left all five workers idle.
- The stdout download did, and that was a defect rather than an argument for async.
- The view buffered into memory the very file the model writes to disk to keep off it.
- #972 streams it: 21 MB to serve an 11.6 MB download, where it was 45 MB.
Where I land
None of it has to break things the way the AAP redesign did, and nothing anyone asks for is blocked by it.
The one I would start now is the API contract, since a generated client breaks nothing.
@cigamit, the rest can wait for a feature that needs them, or for the ASGI move to give us numbers.