Files
Stalwart-webui/SCHEMA_DEVIATIONS.md
T
Steven RYDELL 101b24b361 fix(schema): track client-side schema deviations, restore schema.ts fidelity
src/types/schema.ts had drifted from the official schema contract: a
`clientOnly` field had been added to `FilterEnum` to support client-side
log filtering. Restored it to match upstream exactly and moved the
deviation-only type into a new intersection type in
src/lib/schemaDeviationTypes.ts instead.

Audited the codebase for every place the UI does something the official
schema doesn't support and tagged each with `// SCHEMA-DEVIATION: <id>`,
documented in the new SCHEMA_DEVIATIONS.md registry (what, why, and the
ideal server-side fix):

- log-client-filters: Level/Event filters on Log Entries, applied
  client-side because x:Log/query rejects them as JMAP filters.
- account-quota-usage-column: synthetic quotaUsage column on the
  Accounts list.
- mailbox-client-hierarchy-sort: full-fetch + client-side sort to
  reconstruct mailbox parent/child hierarchy.
- webapp-enabled-column-fallback: synthetic "Enabled" column label for
  x:Application when the schema list doesn't define one.

Other viewName/objectName special cases (x:OtpAuth, x:Expression,
x:Rate, x:Action, x:Trace, CustomComponent/*) were checked against
upstream and are pre-existing architecture, not fork deviations.
2026-08-01 17:54:08 +02:00

6.2 KiB

Schema deviations

Stalwart WebUI is schema-driven: the server's JSON schema is the single source of truth for what forms, fields, filters, and columns exist. This fork tries to stay aligned with that philosophy (see stalwartlabs/webui#discussion and the maintainer's note on why exceptions belong server-side, not in the UI).

Everything in this file is a deliberate exception: a place where the UI does something the schema doesn't (yet) describe, because the equivalent server-side capability doesn't exist in stalwartlabs/stalwart or stalwartlabs/webui. Each entry is tagged in code with // SCHEMA-DEVIATION: <id> so they're greppable (grep -rn "SCHEMA-DEVIATION" src/).

The goal is not to remove these — they're real functionality this fork wants to keep — but to track them separately from schema-driven code, so it's always clear which is which, and so each one can be dropped the day the server (or upstream webui) grows the equivalent native capability.

src/types/schema.ts is never touched for a deviation

src/types/schema.ts mirrors the server's schema contract exactly and must stay aligned with the official webui/server types — it is never edited to accommodate a deviation, even to add an extra optional field.

If a deviation needs to carry extra data on an otherwise-official schema shape (e.g. a flag consumed only by the deviation's own code), the augmented type lives in the deviation's own module or in src/lib/schemaDeviationTypes.ts, as an intersection with the official type (OfficialType & { extra?: ... }), and is imported only where the deviation is actually used. schema.ts itself stays byte-for-byte alignable with upstream's version of the file.

Status legend

  • 🟡 Workaround — client-only, would be removed if the server supported it natively.
  • 🔵 Upstream tracked — an issue has been filed upstream; link included.

Deviations

log-client-filters 🟡

  • Where: src/lib/logFilters.ts, type augmentation in src/lib/schemaDeviationTypes.ts
  • What: injects level and event as filterable columns on the x:Log list, applied entirely client-side (clientOnly flag consumed by DynamicList). The clientOnly flag is declared as ClientOnlyFilterEnum (an intersection type), not on the official FilterEnum in schema.ts.
  • Why: Stalwart's JMAP x:Log/query returns unsupportedFilter for both properties today, even though they're returned per row.
  • Ideal fix: stalwartlabs/stalwart accepts level/event as real query filters; the schema then advertises them normally and logFilters.ts + the ClientOnlyFilterEnum augmentation are deleted.

account-quota-usage-column 🟡

  • Where: src/lib/accountColumns.ts
  • What: adds a synthetic quotaUsage column to the x:Account/User list that isn't a real schema property — DynamicList resolves it from the usedDiskQuota + quotas.maxDiskQuota pair and formats it specially. Also re-adds roles (a real property, just not in the list's default columns).
  • Why: the Accounts list schema doesn't expose usage/role as list columns, only as detail-view fields.
  • Ideal fix: the server's x:Account/User list schema includes roles and a computed usage/quota column natively; this file is deleted.

mailbox-client-hierarchy-sort 🟡

  • Where: src/components/lists/DynamicList.tsxsortMailboxesByHierarchy and the isMailboxList branch
  • What: for the Mailbox list, fetches the entire result set (bypassing normal server pagination) and sorts it client-side so each parent mailbox is immediately followed by its children, with indentation depth tracked in React state.
  • Why: the server returns mailboxes in whatever order the query produces, not grouped by parent/child, and a mailbox's parent can land on a different page than the mailbox itself, so hierarchy can't be reconstructed one page at a time.
  • Ideal fix: the server offers a native tree/hierarchical ordering (or a sort that groups by ancestry) for Mailbox/query; the client-side full-fetch-and-sort is deleted in favor of normal paginated queries.

webapp-enabled-column-fallback 🟡

  • Where: src/components/lists/DynamicList.tsxdisplayColumns in the isWebApplications branch
  • What: reordering the x:Application list's real schema columns (Description first, Enabled second) is not a deviation, but if the schema's list.columns doesn't include an enabled column at all, a fallback column definition with a hardcoded label is fabricated client-side so the toggle still renders.
  • Why: the x:Application list schema is not guaranteed to expose enabled as a list column, even though it's a real object property (fetched separately via properties.push('enabled')).
  • Ideal fix: the server's x:Application list schema always includes enabled as a real column; the fallback branch is deleted (only the reordering logic remains, which is not a deviation).

Not a deviation (for reference)

A few other viewName === '...' / objectName === '...' checks exist in DynamicList.tsx, MainContent.tsx, Sidebar.tsx, layout.ts, and FieldWidget.tsx (e.g. x:OtpAuth, x:Expression, x:Rate, x:Action, x:Trace, CustomComponent/Dashboard and other CustomComponent/* pages, the x:Application column reordering itself, the active-WebApp info card). These are not tracked here: they render real schema data with a custom widget or extra display, the same pattern already used upstream for special object types — they don't fabricate data or bypass the server's filtering/pagination. Verified against upstream/main for each: the object/view names above already drive special-cased rendering there too, except x:Application/Mailbox/x:Log/x:Account/User which are fork-only and covered by the entries above (or explicitly noted as presentation-only, e.g. the Web Applications column reorder).