A scale-2 money field over a four-decimal column (the documented fee
example) was served "12.5000" and answered money.too_precise when the
untouched value was saved back. Trailing zeros carry no precision, so the
server check and the SPA parser count only significant fraction digits;
the value still reaches the column unchanged, never rounded or truncated.
- cabana validateMoneyValue and admin parseMoneyInput trim trailing zeros
before the scale comparison
- forms docs and cabana README name the rule; embedded dist rebuilt
- MoneyField spec asserts 12.5000 at scale 2 is accepted, 12.5010 refused
- type: password on a settings form binds a lagoon.Encrypted column as a
write-only secret (any other column fails boot naming the field): the
GET omits it from data and reports stored, env or empty per field in
meta.secrets; on PUT an absent key or "" keeps the value, null clears
it, a non-string is a 422; the answer never carries a value
- cabana.SecretFallbacks (field to config key) gives the env state;
cabana.SettingsMeta, SecretStored/SecretEnv/SecretEmpty exported
- settings forms accept richeditor: the Put sanitizes through seaglass
like a controller save and returns meta.notices; repeater and
dependsOn stay refused (settingsFormOnly next to controllerFormOnly)
- backend::lang secret.* phrases in en and pl
- admin SPA: PasswordField secret status line (stored with Remove stored
value, env, empty, pending clear with Undo), SettingsFormView tabs with
error-tab activation and the secrets/notices meta, settings payload
that never sends an untouched secret; regenerated OpenAPI and TS
types; rebuilt dist
- vault fixture, TestSettingsSecretCompile, Docker TestSettingsSecretSmoke
and TestSettingsRichEditorSmoke, conform settings fixture with a
password field; settings.md sections, forms.md notes, cabana README
- columns.yaml type: money with currency or currencyFrom (exactly one),
never searchable, not sortable by default; every list row carries the
currencyFrom column's value under its own key; the schema serves
currency and currencyFrom on the column
- pact.AdminRecordAction.Danger, served as danger on cabana.RecordAction
(absent when false); RecordActions.vue passes it to the confirm dialog
- CellValue money kind through formatMoney with the row's code, the em
dash when empty and the amount alone without a code; DataTable aligns
money headers and cells to the right
- PreviewView renders Edit only when a registered value field allows
update (F6)
- main.css .summer-table* and .summer-stat__note kit classes (F7) over
--c-* variables; cabana README kit rows
- fixture record actions void (danger) and resend; TestMoneyListCompile,
TestMoneyListSmoke and TestRecordActionDangerSmoke; regenerated OpenAPI
and TS types; rebuilt dist; lists-and-filters and admin-controllers docs
- type: money on fields.yaml with currency or currencyFrom, scale (2..4,
default 4) and max; boot fails naming the field on two sources, none,
a bad scale or max, a column that is not a string or a Scanner/Valuer,
or a currencyFrom that is not a string column
- a save answers 422 money.invalid, money.too_precise or money.too_large
on the field and fills the accepted decimal string unchanged; the same
check runs on controller, settings and relation child saves
- backend::lang money.* phrases in en and pl
- admin SPA: money.ts (parseMoneyInput, trimMoneyDisplay, formatMoney over
Intl with the string itself), MoneyField.vue with the currency suffix,
blur validation and the read-only box, preview through the control,
the field scope on PreviewView; regenerated OpenAPI and TS types,
rebuilt modules/boardwalk/dist
- acme.payments fixture with TestMoneyFieldCompile and the Docker
TestMoneyFieldSmoke; forms.md Money section and cabana README
Client-side canSubmit now matches the 422 rule so Submit stays disabled
with an empty scope set (WR-02).
Co-authored-by: Cursor <cursoragent@cursor.com>
- MLPluginField wraps PluginField in the MLField locale switcher; the
element gets the active content locale and that locale's string
- isMLField treats a translatable plugin field as a locale map in the form
state, FormView adopt and the relation child modal
- summer-upload posts the file with the admin session to the field upload
route and answers summer-upload-result {id, url} or {id, error}
- noticeLines shows meta.notices under each field as a warning status line
(text only) and in the saved toast; the stored value is reloaded
- docs/backend/field-types.md documents the full element contract;
modules/boardwalk/dist rebuilt
- loadFieldTypes fetches GET /field-types after sign-in, loads each script
once and adds the styles as global links activateStyles never disables
- PluginField creates the type's element, sets the value property and the
contract attributes, takes summer-change as the form value and never
writes back a value the element just emitted
- rendererForField and isRegisteredField route any field with an element
to PluginField in forms, previews and the save payload
- rebuilt modules/boardwalk/dist
- Store list queries under summer-admin.list:<admin id>:<controller>; me()
and setCurrentUser set the owner, which removes other admins' keys and
the unscoped keys of older builds; nothing is read or written without one
- clearUser removes every stored query, so logout, session expiry and an
unrecoverable 401 (expireSession, now used by main.ts) all clear them
- A list request built from a restored query that fails for any reason
(422, 500, network) forgets the query and loads the plain list once
- A failing URL query keeps the error and offers a reset button
(backend::lang.list.reset_list, en and pl)
- Document remembered list state in docs/backend/lists-and-filters.md
- Rebuild modules/boardwalk/dist
- listState.ts stores each list's query (search, sort, dir, page, per_page,
filter[name]) under summer-admin.list:<controller>; an empty query is
never stored, malformed stored values are dropped like a hand-edited URL
- ListView restores the stored query into the URL before its first list
request when entered without list params; a URL with list params wins
- a 422 forgets the stored query; an out-of-range page clamps to last_page
- logout clears every stored list query
- check-phase10.sh allows browser storage in useSidebar.ts and listState.ts
- list smoke tests start from a clean list state
- rebuilt modules/boardwalk/dist
- cabana.BackendUser is an attach.Owner (MorphName Backend\Models\User,
cabana.BackendUserMorphType) with one public attachOne avatar
(cabana.BackendUserAvatarField), as WinterCMS stores it
- POST /account/avatar stores one file_data image (jpg, jpeg, png, gif or
webp, image guard, at most cabana.AvatarMaxBytes = 2 MiB, under a
server-generated name) and replaces the previous avatar: old rows are
deleted in the transaction, their blobs after the commit
- DELETE /account/avatar removes the avatar and is idempotent
- GET /auth/me and the account routes return avatar {url, thumb_url}
(250x250 crop); cabana.BackendUserAvatar lets plugins read it
- swag annotations, regenerated admin.json and schema.d.ts; route
inventories, CSRF walk (29) and OpenAPI conformance learn both routes
- SPA: avatar upload and remove on My account, avatar image in the user
menu trigger and header; backend::lang.myaccount avatar strings
- rebuilt boardwalk dist; cabana README, users-and-permissions and
admin-spa docs
- lagoon: 202610090001_backend_users_metadata adds WinterCMS's nullable
jsonable backend_users.metadata column (TEXT); rollback drops only it
- cabana.BackendUser.Metadata and Subtitle read metadata.subtitle leniently
(NULL, blank, null, [], invalid JSON and non-strings read as empty)
- GET /auth/me returns subtitle; PUT /account edits the signed-in admin's
own names, email and subtitle, and the password with current_password;
any other key is a 422 and nothing is written
- email: required, valid, 6-255 chars, unique against other admins' email
and login ignoring case (soft-deleted included); a unique-index race is 422
- a password change sets tokens_valid_after, revokes the presented token and
gives a cookie session a fresh cookie
- swag annotation, regenerated admin.json and schema.d.ts; route inventories,
CSRF walk (27) and OpenAPI conformance learn the route
- SPA: My account item in the user menu opens /account (AccountView), which
saves through PUT /account and updates the top bar via setCurrentUser
- backend::lang.myaccount strings (en, pl), rebuilt boardwalk dist
- cabana and lagoon READMEs, users-and-permissions and admin-spa docs
A settings entry with a controller opens that controller's route from
its card (settingsPath); singleton cards still open /settings/<code>.
On a linked controller with no navigation entry of its own the rail
marks Settings current, the breadcrumbs read Settings > entry label
(plus the record title on record and create routes) and the page title
uses the entry label. The settings docs describe the admin behaviour;
the embedded admin shell is rebuilt.
The admin sets the html font size to 14px, so the rem utilities w-11 and
size-5 shrank the track to 38.5px and the knob to 17.5px against the
fixed 26px height and 18px travel. Use w-[44px] and size-[20px] so the
knob sits in a symmetric 3px inset at both ends. Rebuild the embedded
admin shell.
- MLField.vue is the one locale-switching wrapper; MLTextField,
MLMarkdownField and the new MLTextareaField are shells around it
- isMLFieldType replaces the inline ML type-name checks in formState,
FormView and RelationChildModal
- register mltextarea as a group-labelled control; rebuild dist
- formState: changedLocales and presetUpdates hold the preset-follow rules
for text and mltext pairs
- FormView applies presetUpdates on create, tracks per-locale hand edits of
ML fields and follows the active ML locale
- rebuilt modules/boardwalk/dist
- MarkdownField posts the source to POST /markdown/preview when Preview
opens and again 300 ms after a change while open; stale answers dropped
- the pane binds only data.html of a 2xx answer; a refusal is a text notice
- mlmarkdown previews the active locale and follows a locale switch
- .summer-markdown style kit restores headings, lists, code and tables
- vitest coverage, forms.md and rebuilt modules/boardwalk/dist
Copy overwrote the active locale with the other locale's often-empty string, so the editor went blank. Keep a single synchronized locale picker.
Co-authored-by: Cursor <cursoragent@cursor.com>
MLMarkdownField and relation-child forms share the enabled-locale
inject and adopt merge; OpenAPI, types, docs, and boardwalk dist match.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Document markdown, mltext, and mlmarkdown plus TranslationWriter save semantics
- Extend the SPA registry tests and rebuild committed boardwalk dist
Co-authored-by: Cursor <cursoragent@cursor.com>
- WidgetField posts the summer-action event's detail.payload as payload only
when the detail carries one; a payload-less post body is unchanged
- after a successful action the response data is set on the element as the
data attribute (removed when the answer has none) and announced with a
summer-result event carrying {data, fill, message}; failures dispatch nothing
- WIDGET_RESULT_EVENT exported from formContext; unit tests for both directions
- modules/boardwalk/dist rebuilt
Ported plugins send Winter icon-* class names on nav and settings; the SPA
never loads Font Awesome, so unknown names rendered as empty squares. Map
BM Studies, Quizzes, icon-pencil, and a closed Winter backend alias table
onto named @lucide/vue 1.17.0 exports, keep Square for unknown names, and
rebuild the embedded boardwalk dist.
- FieldRelationContract.WritableForeignKey makes a belongsTo field over a
protected foreign key writable; the protected key list is unchanged
- cabana.RelationLockProvider names related ids an administrator may not add
or remove: options and labels carry locked, and a create or update that
changes the locked subset is 403 before any row is written
- columns.yaml invisible keeps a column searchable and out of the rows
- a controller implementing pact.FilterOptions serves a scope filter's
choices before the model
- SPA: locked chips and options in RelationField, DataTable skips invisible
columns
- README, docs, OpenAPI document, TS types and dist updated
- type: permissioneditor with mode radio (1, -1) or checkbox (1); the
controller serves the options per request through
cabana.PermissionEditorProvider and reads and stores the values
- a save answers 422 for a non-object, an unknown code or a value outside the
mode's set and 403 for a changed locked code; stored codes that are not
offered are kept
- record responses carry the stored permissions as an object
- SPA: PermissionEditorField with sections by tab, locked rows and a read-only
mode for the preview
- README, docs, OpenAPI document, TS types and dist updated
- pact.FormVirtualFields lists form fields that are not model columns: never
bound, filled or projected; their values reach the Form hooks through
cabana.VirtualFieldsFromContext when the field's context allows the operation
- type: password is a masked field that must be listed as virtual
- pact.FormRules supplies the rule set per operation and replaces the model's
Rules() for admin saves; a rule on a virtual field sees the submitted value
- preset on a text field follows another text field on the create form
- SPA: PasswordField, preset handling in FormView, empty password left out of
an update
- README, docs, OpenAPI document, TS types and dist updated
- config_form.yaml preview block (optional headerPartial), reported in the form schema as preview
- fields with context: preview show only on the preview screen and are never written
- form messages preview and edit; recordActions without a preview block stops boot
- SPA route {id}/preview, PreviewView and PreviewField, record actions in the footer
- mapWinterUrl maps preview/:id; the update form returns to the preview
- summer-callout partial style classes for status hints
- README, docs, OpenAPI document, TS types and the embedded build updated
- hooks and bulk, record, toolbar and widget actions may return it
- 403 forbidden with the localized message and field details; the write's
transaction is rolled back; other errors stay the opaque 500
- form shows a refused save as a persistent banner and keeps the values;
a refused delete is a toast
- smoke tests, OpenAPI notes, dist, README, docs
- pact.ListRowStates with the fixed RowState set deleted, negative, disabled
- list response meta.row_states keyed by row id; unknown values dropped
- list messages rowStateDeleted, rowStateNegative, rowStateDisabled
- update writes through the scope the load used, so a soft-deleted record
a controller includes stays soft-deleted
- DataTable row state badges and text styles
- roster fixture, smoke tests, OpenAPI, TS types, dist, READMEs, docs
- pact.HasAdminBulkActions with AdminBulkAction, its input and result
- config_list.yaml bulkActions, compiled fail-loud, needs showCheckboxes
- POST .../{controller}/bulk/{action}: ids resolved and locked through the
list scope in one transaction; partial selection is 409
- list schema offers declared actions per principal, with confirm text
- admin SPA bulk actions menu with confirm, busy state and failure toasts
- acme.roster fixture, tracer test, OpenAPI, TS types, dist, READMEs, docs
BM UAT on v0.1.1 showed a date-only calendar for datetime fields, a stuck discard dialog, and raw ISO timestamps when columns.yaml omitted type. The picker now edits time in the popover, confirm sits above the calendar, and omitted time.Time / Date / TimeOfDay columns compile as datetime / date / time.
Co-authored-by: Cursor <cursoragent@cursor.com>
The partial-segment check read only DateField segments, so a time-mode
field (Reka TimeField, data-reka-time-field-segment) with some segments
typed was never flagged. Read both segment attributes. DatepickerField
tests cover both UI-SPEC backstops (Escape focus and disabled days, the
fixed-zone datetime round trip) and the partly filled time case.
- RelationManager renders create/link/delete/unlink in declared order with
one primary, opens the child modal (update or view form) or the pivot
modal on row click, and deletes selected children behind a busy confirm
- RelationChildModal creates and edits children with its own session key
(X-Child-Session-Key) so uploads and dates work inside it
- RelationPivotModal edits link details; the picker links one record with
its pivot values when the relation has a pivot form
- deferrable managers render on the create screen with owner id 0, the
form's X-Session-Key and the pending note, and mark the form dirty
- the registry resolves RelationManager lazily (child forms close an
import cycle); DataTable gains openable rows and a trailing cell
- relation lang keys in en and pl; rebuilt boardwalk dist
- pin @internationalized/date 3.12.4 as a direct admin dependency (approved)
- dateFormat.ts parses and emits date, datetime (local display, UTC emit,
ignoreTimezone wall clock) and time values without the global Date
- DatepickerField on Reka DatePicker and TimeField with locale segments,
calendar popover, clear button, min/max and yearRange bounds
- list cells of type date and time render the stored string
- datepicker lang keys in en and pl; rebuilt boardwalk dist
- sessionKey.ts: one 32-byte base64url key per form mount, sent only in headers
- api/files.ts: FileRoutes over the record and child file routes, XHR upload with progress, 401 refresh and retry
- FileuploadField and FileCaptionModal per UI-SPEC section 3: dropzone, image grid, rows, per-item states, client pre-checks, reorder, protected previews
- FormView provides FORM_SESSION, counts pending changes as dirty and sends X-Session-Key on create and update
- fileupload lang keys in en and pl, admin-spa docs note, deferred smoke test, rebuilt dist
loadControllerAssets no longer activates stylesheets. activateStyles,
called only by the router, records the controller on screen, and new
links are created disabled unless they belong to it, so a list or form
schema that resolves after its view was left adds its links disabled
instead of switching plugin CSS to the wrong controller. Rebuilt the
embedded admin dist.
The router now drives stylesheet activation after every confirmed
navigation: the list, create and record routes enable their controller's
links, and settings, login, not-found and a controller whose schema has
not arrived yet enable none. Rebuilt the embedded admin dist.
The URL parser resolves %2e%2e like .., so /{base}/assets/%2e%2e/api/...
passed assetAllowed and would load from outside the asset prefix. A
segment is now a dot segment after decoding %2e, in any case.
- tests/app/pluginAssets.test.ts covers the URL check, loadScript,
loadStyles, activateStyles and loadControllerAssets
- modules/boardwalk/dist rebuilt
- ListToolbar renders server-filtered actions after delete as outline buttons, busy during their POST
- ListView posts {} to toolbar/{action}, toasts, reloads the list and refetches the header partial
- Lists load controller assets too, so other controllers' stylesheet links are disabled on every open
- Vite dev server proxies {prefix}/assets to summer serve; dist rebuilt
- partialNodes rebuilds the server node tree with h() under the server's tag, attribute and URL lists
- PartialHost owns the skeleton, empty and failure states and keeps nodes visible on refetch
- type: partial is a valueless group-labelled field rendered on create and update
- ListView shows headerPartial above the list card and refetches it after bulk delete
- summer-partial and summer-stats style kit in main.css, documented in the cabana README; dist rebuilt