Files
komp_ac/web/README.md
2026-08-11 14:33:24 +02:00

191 lines
9.5 KiB
Markdown

# Web admin and analytics
An Axum SSR/HTMX web frontend for the existing komp_ac gRPC backend. It provides
the browser version of the client admin panel and the ECharts analytics view.
The browser never connects to gRPC directly: Axum authenticates with an
HTTP-only cookie, creates typed Tonic requests, and returns full HTML pages or
HTMX fragments.
The backend remains the authority; this crate does not add another HTTP API to it.
## Run
The web UI is a library started by the main server; it is no longer a standalone
binary. From the parent workspace, start the server normally:
```sh
cargo run -p server -- server
```
Open <http://127.0.0.1:3000/login> to log in. The admin panel is at
<http://127.0.0.1:3000/admin>, roles and permissions are at
<http://127.0.0.1:3000/permissions>, and analytics remains at
<http://127.0.0.1:3000>.
The access token is kept in an HTTP-only cookie. The default gRPC endpoint is
`http://[::1]:50051`. Both addresses can be changed:
```sh
ANALYTICS_GRPC_ENDPOINT=http://127.0.0.1:50051 \
LISTEN_ADDRESS=127.0.0.1:8080 \
cargo run -p server -- server
```
## Structure
HTML lives in `.html` files, never in Rust strings. Templates are
[askama](https://docs.rs/askama): compiled at build time, type-checked against
their struct, and HTML-escaped automatically.
**`templates/` mirrors `src/` directory for directory.** The Rust for a page
and the markup for that page live at the same path in the two trees, and
`src/pages/` itself matches the client crate — the same page names, the same
`loader / logic / state / ui` split. A `#[template(path = …)]` is therefore the
module's own path, which is also where the file is in `templates/`.
```
src/ templates/
ui/ ui/
mod.rs base.html <html>, <head>, navbar
(Nav, Alert, Notice, navbar.html the one navbar
ErrorPage, render) form_page.html admin form card layout
alert.html error/success macros
alert_fragment.html standalone POST reply
notice.html one-line inline notice
error.html full-page load failure
pages/ pages/
admin/admin/ admin/admin/
mod loader logic state ui admin.html
workspace.html
add_table/ add_table/
mod loader logic state ui add_table.html
add_logic/ add_logic/
… add_logic.html
add_validation/ add_validation/
… field.html rule.html set.html
shared_fields.html
analytics/ analytics/
… analytics.html catalog.html
profile_options.html query_result.html
import_export/import/ import_export/import/
… import.html
import_export/export/ import_export/export/
… export.html
login/ login/
mod logic state ui login.html
permissions/ permissions/
common/ tabs.html the section switcher
loader logic state ui
roles/ roles/
mod loader logic state ui roles.html
users/ users/
mod loader logic state ui users.html
grants/ grants/
mod loader logic state ui grants.html
static/app.css the only stylesheet, at /static/app.css
```
Every full page extends `ui/base.html`, which links the stylesheet and renders
`ui/navbar.html`, so the navbar is on every page without a page opting in. The
six admin forms extend `ui/form_page.html` for the back link and card chrome on
top of that.
Each endpoint maps to exactly one template. The Rust side of the mapping is the
`#[derive(Template)]` struct in the page's `ui.rs`; the template names its
endpoint in a comment on line 1.
| Endpoint | Page directory | Template |
| --- | --- | --- |
| `GET /` | `pages/analytics/` | `analytics.html` |
| `GET /api/profiles` | `pages/analytics/` | `profile_options.html` |
| `POST /api/catalog` | `pages/analytics/` | `catalog.html` |
| `POST /api/query` | `pages/analytics/` | `query_result.html` |
| `GET /login` | `pages/login/` | `login.html` |
| `POST /login` | `pages/login/` | sets the cookie, `hx-redirect` |
| `GET /admin` | `pages/admin/admin/` | `admin.html` |
| `GET /admin/workspace` | `pages/admin/admin/` | `workspace.html` |
| `POST /logout` | `pages/admin/admin/` | clears the cookie, `hx-redirect` |
| `GET /admin/tables/new` | `pages/add_table/` | `add_table.html` |
| `GET /admin/logic/new` | `pages/add_logic/` | `add_logic.html` |
| `GET /admin/validation/new` | `pages/add_validation/` | `field.html` |
| `GET /admin/validation/rules/new` | `pages/add_validation/` | `rule.html` |
| `GET /admin/validation/sets/new` | `pages/add_validation/` | `set.html` |
| `GET /admin/import` | `pages/import_export/import/` | `import.html` |
| `GET /admin/export` | `pages/import_export/export/` | `export.html` |
| `GET /permissions` | `pages/permissions/` | redirect to the first open section |
| `GET /permissions/roles` | `pages/permissions/roles/` | `roles.html` |
| `GET /permissions/users` | `pages/permissions/users/` | `users.html` |
| `GET /permissions/grants` | `pages/permissions/grants/` | `grants.html` |
Every form `POST` answers with `ui/alert_fragment.html`, swapped into the page's
`#submission-status`. Analytics errors use the lighter `ui/notice.html`.
Inside a page module the split is the same everywhere, and the same as the
client: `mod.rs` declares the routes, `loader.rs` calls gRPC and shapes the
result, `state.rs` holds the view model and form parsing, `logic.rs` is the
handlers, and `ui.rs` binds state to the templates in that directory.
## Admin panel
The admin panel mirrors the client workflow with browser-oriented pages:
- profile, table, and physical-column browsing;
- table creation, including indexes, links, money settings, and row labels;
- Steel table-script creation;
- field validations, reusable global rules, validation sets, and set application;
- browser CSV import with live schema validation and chunked bulk insertion;
- CSV downloads for one or multiple tables;
- login, role checks, logout, and navigation to analytics.
CSV files use the same column-header convention as the client. Multi-table CSV
files include a table-name header row before the column-name row. Browser files
are read locally and submitted to Axum; the backend is accessed only through the
existing `TablesData` gRPC service.
## Permissions
Permissions are a nav section of their own, not a page inside the admin panel:
managing people is a different job from designing tables, and the two are held
by different accounts. The section is three pages, one per decision, with a tab
bar between them:
- **`/permissions/roles`** — which roles exist, what they inherit from, how many
people hold each, and removing the ones nobody holds. Creating a role can hand
it starter access (`Read everything` or `Read and write everything`) in the
same submission.
- **`/permissions/users`** — who holds which role, and password resets. Only
users whose role the caller outranks carry controls.
- **`/permissions/grants`** — what a role may do, as one matrix per profile:
objects down the side, `read / insert / update / delete` across the top. A
cell is a grant held directly (click to revoke), an inherited one (changed on
the parent role), or an empty one (click to grant).
Every button on the grants page — one cell, one row's `All`, a profile's
`Full access`, the page's `Full access to everything` — posts the same form to
`POST /permissions/grants/apply`, and differs only in the list of
`object|action` pairs it carries. The lists are built in Rust, so the server is
never asked to work out what "everything" meant, and the shortcuts grant the
wildcard objects (`data:*`, `data:<profile>/*`, `journal:*`, `ecb:*`), which
keeps them covering profiles and tables created later. `ecb:*` is read-only
everywhere, because the server writes it.
The table-definition workspace still edits one table's grants in place; it posts
to the same endpoint and returns to itself.
## Analytics
The first SQL result column is used for category labels. Bar and line charts use
all remaining columns as numeric series, pie uses the second column, and scatter
uses the first two columns. Table view displays every returned value.
The profile selector is populated automatically through
`TableDefinition.GetProfileTree`. Choosing a profile fetches its live public
analytics catalog. The sidebar shows tables, columns, types, and links and can
create starter queries. Its **LLM schema context** section generates a complete
text summary that can be copied into an LLM prompt to request a valid analytics
SQL query.
HTMX, Alpine.js, and ECharts are loaded from jsDelivr, so the browser needs
network access when opening the page. HTMX owns server communication and fragment
swaps; Alpine is limited to browser-local editor, clipboard, and chart behavior.