The web app has no screen code per entity. It reads the metadata document that every module publishes, prunes it for the signed-in user and the tenant, and hands each part to a generic engine. This page explains that path from GET /api/v1/_meta to a painted screen. The metadata classes themselves are listed on metadata model.
In short¶
loadMeta()loads/_meta,/_switches, and/_overlayonce per session and keeps the result in memory (webapp/js/meta.js:16-47).- Three passes run before any screen reads the document: permission pruning, display-switch pruning, and the tenant field overlay (
webapp/js/meta.js:29-38). - A module app turns each sidebar section into a tab by its
view_type(webapp/js/module-app.js:101-139). - A list row opens a record card built from
detail_view.tabs; a card's edit mode is the generic form built fromform.field_groups(webapp/js/gen-list.js:816-844,webapp/js/gen-detail.js:63-139). - Labels resolve through
resolveLabel, which picks the current language and falls back to English (webapp/js/meta.js:284-288).
How it works¶
From document to screen¶
The flowchart shows which engine renders which part of the metadata.
flowchart TD meta["GET /_meta"] --> load["loadMeta"] sw["GET /_switches"] --> load ov["GET /_overlay"] --> load load --> prune["pruneGatedFields and pruneSwitchedOff"] prune --> overlay["applyFieldOverlay"] overlay --> app["makeModuleScreen"] app -->|"view_type list or tabbed_list"| list["makeGenList"] app -->|"view_type tree"| tree["makeGenTree"] app -->|"view_type settings"| settings["makeGenSettings"] app -->|"view_type custom"| custom["getCustomWidget"] list -->|"row tap"| detail["makeGenDetail"] detail -->|"form tabs"| card["mountRecordForm"] card -->|"edit or create"| form["createGenForm"] list -->|"actions"| sheet["openActionSheet"]
The same content as steps:
loadMeta()stores the parsed document inraw(webapp/js/meta.js:27-28).pruneGatedFieldsremoves form fields and action body fields that name apermissionthe user does not hold (webapp/js/meta.js:53-74). The comment states that the API strips such values for everyone else, so the form would otherwise show an empty, unwritable box.pruneSwitchedOffremoves fields, list columns, and sub-table columns whoseswitchis off for the tenant. An unknown switch counts as on (webapp/js/meta.js:80-105).applyFieldOverlayapplies the tenant's field decisions: switched-off shipped fields leave the form and the list, tenant-defined fields join their form group withstorage: "extra", optional list columns and filters are added, and closed fields are kept for read-only display (webapp/js/meta.js:117-231). See tenant fields.makeModuleScreenflattens the module's sidebar into tabs (webapp/js/module-app.js:93-139). See module apps and app scope.- Each tab's render function paints into its own pane, so a late answer from a previous tab lands in a detached node (
webapp/js/module-app.js:183-192).
Section types and their engines¶
SidebarSectionMeta.view_type |
Engine | Uses in module metadata | Source |
|---|---|---|---|
list |
makeGenList |
84 | webapp/js/module-app.js:115-116 |
tabbed_list |
one makeGenList tab per entity_tabs entry |
1 | webapp/js/module-app.js:109-114 |
tree |
makeGenTree |
1 | webapp/js/module-app.js:117-118 |
settings |
makeGenSettings over tabs |
9 | webapp/js/module-app.js:119-125 |
custom |
the widget registered for custom_widget |
29 | webapp/js/module-app.js:126-137 |
detail |
none; the module app skips it | 0 | webapp/js/module-app.js:109-137 |
The usage counts were made with grep -rn 'view_type="<value>"' over src/cluster_erp/modules. A section with hidden_from_bar gets no tab; a section with required_permission gets no tab for a user without that code (webapp/js/module-app.js:105-108).
Entity parts and their engines¶
| Metadata part | Rendered by | Page |
|---|---|---|
ListViewMeta (columns, filters, search_enabled, paginated) |
makeGenList |
generic list |
FormMeta and FieldGroupMeta |
createGenForm, makeGenFormView, mountRecordForm |
generic form |
DetailViewMeta and DetailTabMeta |
makeGenDetail |
generic detail |
TreeViewMeta |
makeGenTree |
generic tree and settings screens |
ActionMeta |
openActionSheet, runEntityAction |
actions and action sheet |
RecordLinkMeta |
the record card's link entries, raised as cluster:open-section |
generic form |
FacetMeta |
facet rows on the party card | generic detail |
AvatarMeta |
avatar.js |
generic detail |
HomeWidgetMeta |
the home screen widget registry | home widgets |
ModuleMeta.provides |
providerOf(capability) |
record documents, viewer, and share |
Labels and titles¶
resolveLabel(label) returns the label in the current language, then the English label, then an empty string (webapp/js/meta.js:284-288). moduleLabel uses the first sidebar group label and falls back to the raw name in title case (webapp/js/meta.js:296-306). recordTitle(row, entityMeta) uses detail_view.title_field, then the first non-empty list column, then display_name, name, title, or code, and never an id (webapp/js/meta.js:392-414). langLabel(row) reads label_<lang> columns of reference rows with English and the code as fallbacks (webapp/js/meta.js:418-421).
Capability lookup¶
providerOf(capability) finds the module whose ModuleMeta.provides declares a capability, so generic code never names the module that offers it (webapp/js/meta.js:423-432). The record documents feature uses the capability record_documents (webapp/js/record-docs.js:23-37). tests/test_metadata_driven_rules.py:63 checks that this provider is declared by capability.
Overlay refresh¶
When a tenant field definition or setting changes, meta.js listens for the data-changed signal with entity type tenant_field_definition or tenant_field_setting and re-applies the overlay to a rebuilt baseline (webapp/js/meta.js:233-256). Open screens keep the metadata they were built with until they are opened again (webapp/js/meta.js:233-235).
Why it is built this way¶
- The project's written engineering rules require that screen, form, list, and action behaviour comes from metadata. See metadata-driven UI.
- The comment in
webapp/js/meta.js:31-35states that the baseline is rebuilt on demand instead of copied at boot because a copy of the 3 MB document cost about 100 ms on a desktop. - The server drops
Nonevalues from the document. The comment insrc/cluster_erp/server/app.py:1534-1537(at commit 92f8051) states that nulls were half of the document and every client reads a missing key as not set.
Rules and boundaries¶
- Generic engines name no business module.
tests/test_architecture_boundaries.py:75scansgen-list.js,gen-detail.js,gen-form.js,entity.js, andgen-settings.js. - The server checks at start-up that a
sub_entity_tabletab points at an entity with columns (src/cluster_erp/server/app.py:547-589at commit 92f8051). - The metadata document is built once per server process. A metadata change takes effect after a restart (
src/cluster_erp/server/app.py:1530-1543).
Interaction with other parts¶
- The desktop client renders the same document; see the desktop section.
- Permission codes that the client checks with
hasPermissionare seeded from the same metadata; see roles and permissions.
Limits and open points¶
EntityMeta.lock(RecordLockMeta) is not read by any file inwebapp/js/; a search for.lockfound no reader. The lock is enforced by the server's services.- A custom section whose widget is not registered is skipped with a console error instead of a broken tab (
webapp/js/module-app.js:126-137).
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| metadata store | loadMeta |
The client-side copy of the server metadata that is loaded once, pruned, and served to the generic screens. | metadata deposu | webapp/js/meta.js:16 |
| field overlay | applyFieldOverlay |
The pass that lays the tenant's field decisions over the shipped forms and lists. | alan katmanı | webapp/js/meta.js:117 |
| capability provider | providerOf |
The lookup that finds the module offering a declared capability. | yetenek sağlayıcısı | webapp/js/meta.js:425 |
Related pages¶
- Web app platform
- Metadata model
- Module apps and app scope
- Add a screen with metadata
- Metadata-driven UI
Verified against commit 92f8051 on 2026-11-13.