web fixes
This commit is contained in:
240
web-reality-gaps.md
Normal file
240
web-reality-gaps.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# Where the web UI disagreed with the server
|
||||
|
||||
Thirteen places where the Add-table builder and the append-columns panel offered,
|
||||
promised or accepted something the backend does not do. All thirteen are fixed in
|
||||
the `web` crate; the server is untouched. Ordered by severity.
|
||||
|
||||
Severity is what the gap costs the user:
|
||||
|
||||
- **Blocking** — the UI leads you to a definition the server refuses outright.
|
||||
- **Wrong** — the UI states something untrue about the table you are creating.
|
||||
- **Missing** — a real backend capability the UI gives you no way to reach.
|
||||
- **Misleading** — accurate enough to act on, wrong enough to confuse.
|
||||
|
||||
`cargo test -p web`: **158 passed**, up from 134. Clippy clean for every file
|
||||
touched.
|
||||
|
||||
---
|
||||
|
||||
## 1. Indexing a FKlink — blocking
|
||||
|
||||
**Was:** the column panel offered `Indexing: yes/no` for a link like any other
|
||||
column, and the column list gave it an index toggle.
|
||||
|
||||
**Reality:** a link is a foreign key, and the server builds an index for every
|
||||
one of them as it creates the table
|
||||
(`server/src/table_definition/managed_table.rs:361`). Naming a link in `indexes`
|
||||
is not redundant — it is refused:
|
||||
|
||||
```
|
||||
Link 'billing_customer' is indexed automatically
|
||||
```
|
||||
|
||||
`post_table_definition.rs:514`, and the same rule on the append path at
|
||||
`add_table_columns.rs:322`. So the whole table failed to create, after the
|
||||
Create button, for a checkbox the UI itself offered.
|
||||
|
||||
**Now:** `ColumnDraft::is_indexable` is false for a link, the Indexing field is
|
||||
replaced by *"Indexed automatically — a link is a foreign key, and the server
|
||||
indexes every one of them"*, the column list reports `indexed automatically`
|
||||
with no toggle, `selected_index_names` can never name a link, and a rebuilt or
|
||||
tampered post is refused here with the server's own reason instead of being
|
||||
forwarded. Both screens.
|
||||
|
||||
## 2. Global tables and the columns that post to a profile's books — blocking
|
||||
|
||||
**Was:** the shared scope offered ACCOUNTING, ACCOUNTING_TRANSFER and the
|
||||
quantity-ledger switch exactly as a profile scope does.
|
||||
|
||||
**Reality:** `post_table_definition.rs:280` — *"Global tables cannot use
|
||||
accounting or quantity-ledger columns"*. A shared table belongs to every profile
|
||||
at once, so there is no one set of books for it to post to.
|
||||
|
||||
**Now:** the two types are not offered in the shared scope, the quantity-ledger
|
||||
field is not rendered there, and both are refused with the reason. Switching an
|
||||
existing draft to the shared scope reports it *at the switch* rather than at
|
||||
save — the picker going quiet is not an explanation on its own.
|
||||
|
||||
## 3. Table names could be 63 characters — blocking
|
||||
|
||||
**Was:** `validate_identifier(…, "Table name")`, the plain 63-character
|
||||
identifier rule.
|
||||
|
||||
**Reality:** a table name is a prefix that longer identifiers are built from —
|
||||
`idx_<table>_<column>_fk` has to fit in 63 too — so the server's limit is
|
||||
**38** (`catalog/object_naming.rs:35`). Anything from 39 to 63 characters passed
|
||||
the browser and failed at the backend.
|
||||
|
||||
**Now:** `validate_table_name`, with the limit derived from the same arithmetic
|
||||
and the same `common` constant the server reads, so a system column added there
|
||||
moves it here too. The test asserts it lands on 38.
|
||||
|
||||
## 4. Reserved table names — blocking
|
||||
|
||||
**Was:** nothing stopped a table called `accounts` or `general_ledger`.
|
||||
|
||||
**Reality:** every profile is given `general_ledger`, `journal_lines`,
|
||||
`quantity_ledger`, `accounts` and `custom_exchange_rates` when it is created;
|
||||
`post_table_definition.rs:221` refuses the names.
|
||||
|
||||
**Now:** refused in the builder, naming the table and why.
|
||||
|
||||
## 5. `required` was unreachable — missing
|
||||
|
||||
**Was:** `proto_columns` hardcoded `required: false`. No control anywhere.
|
||||
|
||||
**Reality:** `required` is a live column property. The server stores it and
|
||||
enforces it on every row written
|
||||
(`table_validation/runtime.rs:75`) — a row that omits a required column is
|
||||
rejected. The web UI could not produce a required column at all, and could not
|
||||
tell you a column was one.
|
||||
|
||||
**Now:** a Required field on both column panels, carried through the form,
|
||||
listed as a `required` tag, shown in the preview and sent on the request.
|
||||
|
||||
## 6. The append screen could not link to a shared table — missing
|
||||
|
||||
**Was:** its link-target picker read `page.tables`, which for a profile scope is
|
||||
that profile's own tables.
|
||||
|
||||
**Reality:** the server resolves a link against the profile's tables **or** any
|
||||
global one (`post_table_definition.rs:684`). Shared tables — currencies, code
|
||||
lists — were simply absent from the picker with no way to reach them.
|
||||
|
||||
**Now:** link targets are their own list, built by `table_scope::linkable_tables`
|
||||
(the same helper the builder uses), while the browse list stays the scope's own
|
||||
tables. This is the same class of bug as the "No table chosen" one fixed earlier
|
||||
in this session: one question, three answers.
|
||||
|
||||
## 7. A link could point at the table being created — blocking
|
||||
|
||||
**Was:** the builder filtered its own name out of the picker, and that was all.
|
||||
The table-name field has no `hx-trigger` by design, so adding `link(invoice)`
|
||||
and *then* naming the table `invoice` sailed through.
|
||||
|
||||
**Reality:** `post_table_definition.rs:400` — *"Link 'x' cannot point at the
|
||||
table being created"*.
|
||||
|
||||
**Now:** a rule in `validate`, not just a filter on a list, so the order the
|
||||
fields were filled in cannot get round it.
|
||||
|
||||
## 8. `link(accounts)` was refused only by the server — blocking
|
||||
|
||||
**Was:** the builder's loader dropped `accounts` from the picker, but nothing
|
||||
validated a draft that carried the link anyway.
|
||||
|
||||
**Reality:** *"The account relationship is built into ACCOUNTING and cannot be
|
||||
declared as a link"* (`post_table_definition.rs:406`).
|
||||
|
||||
**Now:** a rule in `validate`, shared by both screens.
|
||||
|
||||
## 9. A declared column could collide with a generated one — blocking
|
||||
|
||||
**Was:** duplicate names were checked against the declared columns only.
|
||||
|
||||
**Reality:** the server checks against every name the table will hold, generated
|
||||
ones included — *"Column 'debit' conflicts with a column generated for
|
||||
ACCOUNTING"* (`models.rs:450`). Declaring `debit` beside an ACCOUNTING row was
|
||||
accepted by the builder and refused by the backend.
|
||||
|
||||
**Now:** `ColumnDraft::claimed_names` is the set both `add` and `validate` check,
|
||||
in both directions — the declared column first or the definition row first.
|
||||
Companions named after their own column (`work_phone_ext`) still never collide.
|
||||
|
||||
## 10. Rows could not be identified by a generated column — missing
|
||||
|
||||
**Was:** row-display candidates were the declared, non-compound columns.
|
||||
|
||||
**Reality:** the server accepts any of the table's real column names, generated
|
||||
ones included (`post_table_definition.rs:482`), and even maps a display column
|
||||
of `accounting` onto whatever its `name` companion ended up called. A table whose
|
||||
only readable column is generated — an ACCOUNTING row's `name` — had nothing to
|
||||
be shown by.
|
||||
|
||||
**Now:** every column the table will really hold is a candidate, under its
|
||||
aliased name, and the definition row itself still is not one.
|
||||
|
||||
## 11. The preview omitted `row_revision` — wrong
|
||||
|
||||
**Was:** the preview claims to be "the schema as it will exist" and listed `id`,
|
||||
`deleted`, … `created_at`.
|
||||
|
||||
**Reality:** every managed table also carries `row_revision`
|
||||
(`common/src/system_column.rs:20`). It was also missing from the names an alias
|
||||
may not take, so `row_revision` could be asked for as a generated column's alias
|
||||
and refused by the server.
|
||||
|
||||
**Now:** in the preview and in the reserved set.
|
||||
|
||||
## 12. "already exists in profile ``" — misleading
|
||||
|
||||
**Was:** the duplicate-table-name error always named a profile. In the shared
|
||||
scope there is no profile, so it rendered an empty pair of backticks.
|
||||
|
||||
**Now:** scope-aware. The shared scope says the name has to be free in every
|
||||
profile; a profile scope says the name may also be taken by a shared table,
|
||||
which is what the check really covers.
|
||||
|
||||
## 13. Index and options were reported twice — misleading
|
||||
|
||||
**Was:** `option_label()` included `indexed`, and the column list rendered it as
|
||||
a tag *beside* the Indexed column that said the same thing.
|
||||
|
||||
**Now:** the list's Indexed column is the only place index state is reported;
|
||||
the options column carries required, quantity ledger and currency.
|
||||
|
||||
---
|
||||
|
||||
## Deliberately left alone
|
||||
|
||||
- **The link's `<n>_version` column.** Every link creates one
|
||||
(`managed_table.rs:324`), and the preview does not show it. It is an internal
|
||||
column — `common/src/system_column.rs` says so — never exposed by the data
|
||||
API, so listing it would be noise rather than honesty.
|
||||
- **`account_id` shown as well as `account`.** Both are real: one is the
|
||||
physical column, one is the API name. The preview already explains the
|
||||
relationship.
|
||||
|
||||
---
|
||||
|
||||
## Suggested server changes
|
||||
|
||||
None of these are needed for the fixes above — the web crate works around each
|
||||
one — but each is a place where a client can only be correct by knowing
|
||||
something the backend never tells it.
|
||||
|
||||
1. **`ListColumnTypes` should say which types a shared table may not use.**
|
||||
A `profile_only: bool` on `ColumnType` would let a picker be right without
|
||||
naming ACCOUNTING and ACCOUNTING_TRANSFER in client code. Today
|
||||
`web/src/schema/mod.rs` has to hardcode the pair (`PROFILE_ONLY_TYPES`), and a
|
||||
third type with the same restriction would be silently offered.
|
||||
|
||||
2. **`ListColumnTypes` should say a link is indexed automatically.**
|
||||
Same shape of problem as (1): the rule is real and client-visible, but the
|
||||
only way to know it is to have read `managed_table.rs`. An
|
||||
`indexed_automatically: bool` would make finding #1 impossible to reintroduce
|
||||
in any client.
|
||||
|
||||
3. **The reserved table names should be reportable.**
|
||||
`general_ledger`, `journal_lines`, `quantity_ledger`, `accounts` and
|
||||
`custom_exchange_rates` are constants of three separate server modules. Every
|
||||
client that wants to refuse them early has to copy the list, and will drift
|
||||
when the server adds one. They belong on a capability or catalog response.
|
||||
|
||||
4. **`MAX_TABLE_NAME_LENGTH` should be reportable too**, for the same reason.
|
||||
The web crate now derives it from the same `common` constant the server does,
|
||||
which holds only because both compile the same source file — a coincidence of
|
||||
layout rather than a contract.
|
||||
|
||||
5. **Projection columns are unreachable from any client.**
|
||||
`post_table_definition` handles a `from(link_column.source_column)` field type
|
||||
(`link_column_projection.rs:30`), but `COLUMN_TYPE_CATALOG` does not report it
|
||||
and `ColumnTypeSpelling` has no variant for it — only `Bare`, `Decimal` and
|
||||
`Link`. So a feature that exists in the backend cannot be offered by the web
|
||||
UI or the TUI. Either add the spelling and catalog entry, or the code is dead.
|
||||
|
||||
6. **The append path checks the global quantity-ledger rule only at execute
|
||||
time.** `add_table_columns.rs:238` runs after the table is resolved, so a
|
||||
client has to work out for itself whether the table it is appending to is
|
||||
global. The web crate now infers it from the profile tree; reporting it on the
|
||||
table would be sturdier.
|
||||
Reference in New Issue
Block a user