# Web Application Guidelines **Document Version:** v1 > **Note on Versioning:** > - This document version is independent — reused across projects > - It describes a **server-rendered web interface driven over a websocket** (NiceGUI on FastAPI); the language rules stay in `DESIGN_DOCUMENT_PYTHON.md` > - Where this document and the language document disagree about the same thing, the language document wins for language matters, this one for interface matters ## Related Documents - **README.md** — Project overview, tool descriptions, build instructions - **AGENTS.md** — Rules for AI assistants - **DESIGN_DOCUMENT_PYTHON.md** — Language rules: tooling, typing, logging, layout - **PROJECT.md** — Project goals and current state --- ## 1. What This Stack Is One Python process serves the pages, holds the session state and talks to the database. The browser renders components and sends events back over a websocket; there is no separate front-end build, no REST layer between the screen and the data, and no client-side router. That buys a great deal — a screen is a function, a test can call it, a value is one attribute access away — and it costs three things that shape every rule below: | Property | Consequence | |---|---| | The server holds the UI state | A slow call blocks the screen, and often every screen | | Events cross a websocket | Anything the browser does must reach the server to have happened | | Components are a third-party library | Its escape hatches (slots, CSS layers) follow its rules, not yours | --- ## 2. Layers ``` core/ decisions: models, rules, calculations — no UI, no database repository/ storage: one class per table or aggregate — no UI services/ one wired-up object: connection + repositories + shared services web/ screens: build components, read services, write back api/ HTTP routes for machines ``` Dependencies point one way: `web → services → repository → core`. A core module never imports the web layer, a repository never renders, a screen never opens a connection. **Every screen has a pure counterpart.** What a screen *decides* — which rows, in what order, which cells may be edited, what a value reads as — belongs in a core module that takes data and returns plain structures. The screen is then the part that cannot be unit tested but also barely has logic in it. This is the single most useful rule in this document: it is what makes a complicated screen testable without a browser. --- ## 3. Services: One Connection, One Lock The whole point of a server is that the database connection exists once, not per user: - One process-wide `Services` object, built lazily on first use, holding the connection and every repository built on it - Database drivers are usually **not** safe for concurrent use, so every call goes through one lock: `svc.run(fn, *args)` - A screen never touches the driver, only `svc.run(...)` The lock has a consequence worth writing on the wall: **a slow query freezes the application for everybody**, not just for the person who triggered it. See §9. Replace the lock with a connection pool before the user count makes this untrue — the call sites do not change. --- ## 4. Page and Screen Structure - **Entry point** starts the server, installs logging before anything that can fail, and prints every address it can be reached at - **One page per URL**: a sign-in page and a main page are usually enough; everything else is a dialog opened from the main page - **A screen is a function**: `open_(svc, ...)` builds its components and returns. No global state, no singletons per screen, nothing left registered when it closes - **State lives in one dict per page**, not in module globals: which object is selected, which tab, which ticks. A dict, because it is shared with the nested functions that redraw parts of the page - **Redraw in units, not wholesale.** Mark the parts that change as refreshable and refresh those; rebuilding the page loses scroll position, focus and any transient state the browser holds ### Dialogs Dialogs are for decisions and rare edits — a confirmation, a picker, managing relations. They are not where everyday data is edited: a value a person changes ten times a minute belongs in the table they are already looking at (§6). --- ## 5. Where the Language Belongs Pick one language for the interface and one for the code, and keep them apart: - Code, comments, identifiers, log messages, commit messages: **English** - Labels, notifications, validation messages the user reads: **the user's language** Mixing them inside one screen is what happens by accident when a feature is written in a hurry; mixing them across screens is what the user notices. --- ## 6. Tables Are the Main Idiom For a data application, the table is the interface. Two rules: **Build rows in core, render in web.** A core function turns records into rows and cells — text, whether it can be edited, where it came from, whether it is held by hand. The screen turns that into component properties. Sorting, grouping and "show only what differs" then have unit tests. **Edit in the cell, not in a dialog.** An input in the cell, written when the field is left or Enter is pressed — never per keystroke, which turns one edit into a dozen writes. ### The slot trap Component libraries let you replace a cell with your own markup (a "slot"). Framework event bindings written inside such a slot **may never fire** — the slot is compiled in a scope that is not the one the binding expects. Verified in a browser: the handler was never reached, with no error anywhere. The reliable pattern is to bypass the framework's event system for that one case: - A plain `` with a **native** attribute handler (`onblur`, `onkeydown`) — compiled by the browser itself - The handler calls the framework's own "send an event to the server" function with a small payload: which panel, which record, what value ### Addressing the panel An event that carries an id, and a server-side registry of live panels, is the general shape for anything the browser drives: ``` mount(panel_id, handler) # once per page build, outside any refreshable emit → {panel: , …} # from the browser dispatch → registry[id] # server routes it, ignores unknown ids release on disconnect # a browser that goes away stops answering ``` Two rules that are easy to get wrong: - **Subscribe once per page, never inside the refreshable** that redraws the table. A subscription per redraw saves a typed value once per redraw that has ever happened - **Put the client id in the panel id.** The registry is process-wide; without it, a second person's typing is handed to the first person's screen, written as them --- ## 7. Custom Canvases and Other Hand-Written Markup When a component library has nothing for what you need — a node editor, a diagram — hand-write the markup and keep the browser side thin: - **The browser reports, the server decides.** A gesture (a wire dropped, a box let go) is sent as an event; the state changes on the server, where a test can reach it. Only what is impossible to do server-side — the box following the pointer — stays in the browser - **One id per canvas**, as in §6, so two open canvases cannot answer for each other - **Attach with a retry**: markup and script arrive in separate messages, so the script's entry point polls a few times for its own element instead of assuming it is there - **Sanitisers strip `