Commit Graph

208 Commits

Author SHA1 Message Date
7ef2e4f6cb test: honour -short in Docker-backed TestMain and cabana list-service fixture
TestMain scanned os.Args for a literal -test.short, but go test passes
-test.short=true, so beachcomber, conga, lagoon and lighthouse started a
Postgres container even under -short. Parse the flags and use
testing.Short() instead; skip cabana's newListService fixture under -short
like adminGorm already does.
2026-10-11 14:43:40 +02:00
73074ee257 fix(admin): keep plugin rail and section panel sticky so Settings stays at the viewport bottom 2026-10-11 14:34:24 +02:00
a0bf8ece05 feat(sieve): add the remaining collection helpers and docs mentions
- Add TryMap, Filter, Reduce, KeyBy, GroupBy, Uniq, UniqBy, Partition
  and Find, plus the Set type built with SetOf (Has, Add, Slice).
- Add unit tests at 100% statement coverage and a runnable example for
  every function and Set method.
- Complete the module README: contracts, use cases, Pluck rationale,
  standard-library pointers and the full API reference.
- docs/architecture/introduction.md: add a Utilities row for sieve.
- docs/setup/coming-from-wintercms.md: map Laravel collection methods
  to sieve and narrow the not-provided row to the fluent collection
  object.
2026-10-11 14:27:13 +02:00
6933004b87 feat(sieve): add sieve module with Map 2026-10-11 14:22:56 +02:00
cb4b0a9c75 fix(16.3): disable Create token until a scope is selected
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>
2026-10-11 13:53:32 +02:00
a955d2a3d4 fix(16.3): refresh CSRF coverage golden and blog scaffold layout
Account token POST/DELETE routes made TestPhase10Coverage expect 32 unsafe
routes; token_scopes.go is hand-authored and excluded from the scaffold pin.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 13:53:31 +02:00
78124ab86f test(16.3-04): full Go coverage for tokens, guard, account API and CLI
Pin bouncer Scope serialization, party Activated order, lagoon migration,
compile/overlap/mint/expiry/validation, guard/debounce/resolver,
interactive-only, account token API and CLI on Postgres.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 13:32:11 +02:00
91830e506f test(16.3-04): pin deny-by-default contract at every layer
Scope match/Allows tables, route classification for every phase09Routes
entry, SPA plaintext lifetime backstops, and Postgres owner lifecycle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 13:17:41 +02:00
2030fd881a fix(16.3-03): expect 32 CSRF-gated admin routes after token API
Account token create/revoke added two state-changing mounts; update the
Phase 10 CSRF walk count so the release gate inventory stays green.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 02:20:55 +02:00
60873a3fea docs(16.3-03): ExampleResolveBackend for plugin token auth
- Add compiled ResolveBackend example with acme.blog cover
- Include src= fence in authentication docs (Bearer-only)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 02:17:43 +02:00
1c930b6ff7 feat(16.3-02): list states, show-revoked toggle, and revoke via ConfirmDialog
Add skeleton/empty/error coverage, revoked-row handling, ConfirmDialog
overflow wrap for long names, and rebuild boardwalk dist.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 02:04:54 +02:00
5a6a190e7f feat(16.3-02): tracer — API tokens card on My account with show-once create
Wire ApiTokensPanel after the profile form, add the two-step create dialog,
phrase keys, smoke tracer, and rebuilt boardwalk dist.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 02:02:44 +02:00
e24e03c47d feat(16.3-01): admin:token CLI, party.Activated, READMEs and docs
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 01:55:07 +02:00
ffdb5d00ca feat(16.3-01): account token API with OpenAPI and phrase keys
- Add list/create/revoke/scopes handlers, validation, and soft revoke
- Regenerate admin OpenAPI and TS types; inventory and conformance cases
- Add server-side api_tokens phrase keys (en/pl)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 01:51:11 +02:00
183583e0e9 feat(16.3-01): interactive-only gates, ResolveBackend, last_used stamp
- Refuse g15_ tokens on account/avatar/refresh/logout; gate field-types and markdown
- Publish cookie-less ResolveBackend; debounce last_used_at once per minute
- Reserve account vendor segment; TestTokenGatesSmoke covers the deny path

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 01:47:52 +02:00
b9273c1da7 feat(16.3-01): tracer — g15_ token scopes through composite backend guard
- Add pact.TokenScope/HasTokenScopes and bouncer.ScopeLimit on Principal
- Append backend_api_tokens migration; Allows evaluates scope before superuser
- Composite guard routes Bearer g15_… to token lookup; smoke tests cover path

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 01:46:32 +02:00
602e0412f3 docs: add a Role in the framework section to every module README
Each module README now says where the module sits in the dependency
graph, why it is its own module, and lists use cases for application
developers.
2026-10-11 00:58:17 +02:00
ca1443e9c8 docs: add a name mnemonic to every module README and the root modules table 2026-10-11 00:26:20 +02:00
11be08d680 test(16.2-07): conformance cases for the field-types and field upload routes
- conformCases() lists every case at package level; checkConformInventory
  reports every missing, extra or duplicate key in one sorted failure
- TestPhase10ConformanceInventory runs it without a database, and
  TestPhase10OpenAPIConformance runs it before the testcontainers setup
- GET /field-types decodes into cabana.FieldTypesResponse and serves the
  fixture type's script; POST .../fields/{field}/upload answers 201
  cabana.Envelope[cabana.FieldUploadResponse] for a multipart part file
- the fixture plugin registers conformnote and the gadget form uses it,
  so create, update and show run its Decode and Load (G-16.2-1, D-13, D-18)
2026-10-10 18:33:03 +02:00
f388d27604 fix(16.2): WR-06 hand a translatable Decode only strings
liftMLValues validated only object-shaped submissions, so a scalar
submitted for a translatable plugin field reached Decode as nil, a
float64 or a bool although pact promises a string; a type that asserts
a string panicked into an opaque 500. decodePluginFields now passes a
string through, turns null into the empty string and refuses any other
value with a 422 on the field before Decode runs.
2026-10-10 16:45:05 +02:00
c532529d18 fix(16.2): WR-02 check the whole upload body before Upload stores it
The route called the type's Upload on the streamed part and only then
checked that no second part followed, so a refused request left a
public, unreferenced object in the bucket. It now reads the multipart
body whole (ReadForm, spooling past 8 MiB to a temporary file), checks
it holds exactly one file part named file, and only then calls Upload.
The remaining window (a failed record transaction after Upload, an
element that stopped waiting) is documented.
2026-10-10 16:43:59 +02:00
3158bb5b57 fix(16.2): WR-01 check the declared save before a field upload
The field upload route ran only protect, so the create form of a
controller without the create toolbar button could still write files to
the public bucket. It now refuses with 403 where the save route would:
create for {id} 0, update otherwise (operationDeclared).
2026-10-10 16:42:28 +02:00
3404a4b3ad test(16.2-06): full unit coverage of the plugin field type framework
- pact: FormFieldError message joining, nil pointer, funcs never serialized
- cabana: registry and option boot rules, compile (option kinds, general
  and core keys, CompileForm vs CompileFormWithTypes, settings and relation
  refusal), decode 422/500 split, localized and capped notices, Load,
  ListValue with the request ctx, field writes, field-types and upload
  routes
- TestFieldTypeSavePostgres (Docker host): create, show, untouched and
  changed update, refused update, upload in and out of the record scope
2026-10-10 15:49:35 +02:00
08a20021e3 test(16.2-06): pin the untouched-value skip in cabana and PluginField
- TestFieldTypeUntouched: served values skip Decode, writes and notices;
  one changed locale writes and reports only that locale; canonical JSON
  comparison; a create never skips
- PluginField: an element that echoes its value on connect is never
  written a second time
2026-10-10 15:42:31 +02:00
85fe541b03 feat(admin): View site link next to the user menu
Opens the admin origin's root in a new tab, like the WinterCMS backend.
2026-10-10 14:01:44 +02:00
4adb48a76e feat(16.2-01): translatable plugin fields, element uploads and save notices in the admin
- 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
2026-10-10 11:25:26 +02:00
8fa92db96d feat(16.2-01): translatable plugin field types, untouched skip, writes, list values and uploads
- translatable types lift, store and hydrate through the ML path
  (isMLField); Decode and Load run per locale and notices carry the locale
- on update a value equal to the served value (Load of the stored host
  value or exact translation) is neither decoded nor written
- FieldWrite, FieldWritesFromContext and WithFieldWrites expose what a save
  writes to the Form hooks
- list columns may use a plugin alias and show the type's ListValue
- POST .../{id}/fields/{field}/upload hands the file to the type's Upload
  (guard, X-Requested-With, permissions, field context, record scope, body
  cap); OpenAPI, route inventories, README and docs updated
2026-10-10 11:19:24 +02:00
83fa2f7b2c feat(16.2-01): host plugin field type elements in the admin SPA
- 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
2026-10-10 11:10:41 +02:00
717d46ec3d feat(16.2-01): compile, decode and load plugin field types in cabana
- compileFieldTypes builds a per-boot field type registry (alias, core and
  cross-plugin collisions, element prefix, assets, option keys) passed down
  the compile functions; settings and relation forms refuse plugin aliases
- fields.yaml accepts a plugin alias with the general keys plus the type's
  options (typeOptions, element, translatable in the schema); CheckOptions
  runs at boot; CompileFormWithTypes for plugin tests
- save decodes plugin values inside the transaction (FormFieldError is a
  422, other errors opaque 500) and answers meta.notices (FieldNotice) only
  when a type reported something; show and save serve values through Load
- GET /field-types lists the types with versioned asset URLs; OpenAPI,
  schema.d.ts, README and docs/backend/field-types.md updated
2026-10-10 11:06:12 +02:00
1e20ace15c feat(16.2-01): add the pact plugin field type API
- pact.HasFormFieldTypes and pact.FormFieldType: a plugin registers admin
  form field types under a short alias, with element, assets, options and
  CheckOptions/Decode/Load/ListValue/Upload funcs
- FormFieldValue, FormFieldDecoded (value plus notices), FormFieldNotice,
  FormFieldUpload, FormFieldUploadResult and FormFieldError
- ListValue takes the request context: func(ctx, value) string
2026-10-10 11:03:01 +02:00
cd25af0878 fix(admin): scope remembered list state to the signed-in admin and recover from bad restored queries
- 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
2026-10-09 23:53:19 +02:00
837922cf43 test(cabana): relation subquery and locale order coverage
- TestListRelationSQL: DryRun matrix for belongsToMany (incl. custom pivot keys and self-reference), hasMany, hasOne and belongsTo search and sort, soft delete, 422 refusals, bound and LIKE-escaped search values, no outer JOIN next to ListExtendQuery and scope predicates
- TestListRelationSearchPostgres: exact total through two matching tags, string_agg sort order and hasMany search on Postgres
- TestEnabledLocalesDefaultFirst: default-locale ordering cases
2026-10-09 22:47:57 +02:00
9cac45c389 fix(admin): keep list state in the URL and browser storage
- 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
2026-10-09 21:04:00 +02:00
bd8626e5fc fix(cabana): send the default content locale first in enabledLocales
- enabledContentLocales moves TranslationWriter.DefaultLocale to index 0
  and keeps the other enabled codes in the writer's order
- every ML control opens on enabledLocales[0], so the form now shows the
  host-column values (content, excerpt, title, slug) instead of an empty
  non-default locale
- a DefaultLocale error is logged and keeps the writer's order
- README and forms docs: default locale first
2026-10-09 20:58:12 +02:00
06d75cb654 fix(cabana): search and sort to-many relation columns through subqueries
- relationSubquery reads the pivot, key and table names from GORM's parsed
  relationship (Schema.Relationships.Relations), never from a convention
- list search matches every relation column through a correlated EXISTS,
  so the count query gains no JOIN and belongsToMany search stops failing
- sorting a hasMany or belongsToMany column orders by a correlated
  string_agg subquery before the primary-key tiebreak
- polymorphic relations and unknown related columns answer 422
- DryRun smoke test TestListRelationSearchSmoke needs no Docker
2026-10-09 20:56:53 +02:00
89611eaaf9 test(cabana): cover My account profile, password and avatar
- account_internal_test: metadata subtitle reader and writer (NULL, blank,
  null, [], invalid JSON, non-objects, key preservation, NULL on the last
  key), the attachOne contract, avatar cap, unique-violation mapping,
  English fallbacks, and 401 for non-backend principals on all three routes
- account_test (Postgres): profile payload incl. copied WinterCMS metadata
  and avatar rows, trimmed profile saves and metadata keys, the forbidden-key
  allow-list with unchanged rows, email rules, password rules, the failed
  attempt log, Bearer and cookie session effects, CSRF, avatar upload,
  refusals (SVG, HTML as PNG, size, part shape, 413, no bucket) with no row
  or blob left, replace and remove with blob cleanup
- lagoon: TestBackendAdminMetadataColumn covers type, Winter row round trip,
  rollback of only the column and re-migrate
- SPA: AccountView, the user menu account item and avatar image,
  setCurrentUser and profileInitials; lang fixture gains myaccount keys
2026-10-09 17:59:27 +02:00
4f1cda33d7 fix(cabana): answer an empty or malformed avatar upload body with 422
- POST /account/avatar with no multipart part (NextPart io.EOF) or a broken
  boundary fell through to the generic 500; it is now the invalid-body 422
- a body over the cap still answers 413 payload_too_large
2026-10-09 17:53:21 +02:00
64d21c8caf feat(cabana): add backend user avatars to My account and the top bar
- 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
2026-10-09 17:45:43 +02:00
143eca5a1f feat(cabana): add the My account page with subtitle and password change
- 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
2026-10-09 17:38:04 +02:00
Jakub Zych
9b3803c098 feat(sunrise): update an application in place with summercms.io/update
- new.sh gains --update, run inside one application directory (go.mod and
  summer.yaml required); it changes only that application
- the framework checkout is read from the app's go.mod replace (single-line
  or block form); module-version replaces, missing targets and foreign or
  dirty checkouts are refused through the shared check_framework_dir
- a detached checkout moves fast-forward only to the newest release (or
  SUMMERCMS_REF) through the shared move_detached; a branch checkout is
  pulled with git pull --no-rebase --ff-only, refusing a missing upstream
  or a diverged branch with the checkout unchanged
- other sm-*-app directories on the same checkout are named on one
  "not rebuilt" line and never built or written
- summer is reinstalled, a missing SUMMER_ADMIN__JWT__SECRET is appended to
  this app's .env (openssl, else head and base64; mode kept, never printed),
  then summer build runs and the script ends with the migrate and restart
  reminder and the shared path_hint
- sunrise.UpdateScript() serves the same script with its last line changed
  to main "$@" --update, so truncated downloads still run nothing
- check-phase14.3.sh --forbidden now allows only the literal
  SUMMER_ADMIN__JWT__SECRET in new.sh and still fails on any other
  SUMMER_ name or on a read or export of one; TestUpdateScript and
  TestUpdateScriptRoute are required tests
2026-10-08 20:15:50 +02:00
Jakub Zych
da21670870 feat(surf): print the serve URL and admin status on startup
- Add surf.Router.AdminPrefix (additive): the admin mount path and true
  when BuildRouter mounted the cabana admin, false otherwise
- serve binds with net.Listen before printing, so a busy port fails
  without a misleading URL, then prints the listen address with a
  browser URL and an admin status line: the admin URL, a warning naming
  summer admin:create when backend_users is empty or missing, or
  admin: off with the steps to add an admin controller
- The administrator count is bounded by a 2 s timeout and never fails boot
- route:list ends with the same admin status line
- surf and cabana READMEs and the console and routing docs describe the output
2026-10-08 16:36:21 +02:00
Jakub Zych
14f6982c4d feat(bonfire): add Passthrough and Long command fields
- Command.Passthrough turns off cobra flag parsing and uses arbitrary
  args, so every argument after the command name, including --help, -h
  and --, reaches Run through Input.Args unchanged
- Command.Long sets cobra's Long text for the help command; without it
  the description is shown as before
- both fields are additive: commands without Passthrough parse flags and
  reject unknown ones exactly as before
- README and docs/console/writing-commands.md describe both fields
2026-10-08 15:49:39 +02:00
Jakub Zych
ddf1178c51 feat(sunrise): print the PATH hint for the user's login shell
- new.sh reads the basename of SHELL (via ${SHELL:-} under set -u) and
  prints fish_add_path "<dir>" for fish, which keeps it for new sessions,
  export PATH="<dir>:$PATH" naming ~/.zshrc or ~/.bashrc for zsh and
  bash, and today's generic export line with "your shell profile" for
  any other, empty or unset shell
- dq_escape escapes the install directory for the target shell's double
  quotes: backslash, double quote and dollar always, plus backtick and a
  single-quoted ! for POSIX shells; a per-character loop, so bash 3.2
  and 5.2 behave the same
- the behaviour harness defaults SHELL to empty so cases do not depend on
  the developer's login shell, adds an unset-SHELL mode and covers fish,
  zsh, bash, empty, unset, unknown and two metacharacter directories
- TestBootstrapEndToEnd pins SHELL=/bin/bash
- sunrise README and docs/setup/installation.md describe the per-shell
  hint; the manual PATH section gains fish_add_path (go env GOPATH)/bin
2026-10-08 15:34:42 +02:00
Jakub Zych
93e9a8a45c test(14.3-04): pin the bootstrap script's behaviour matrix and shellcheck
- TestScriptBehavior: 25 cases on a stub PATH (real git, stub go/docker/summer)
  against a bare fake framework remote: usage and name errors, existing app,
  Go missing/old/devel, git required, Docker warning, newest release tag,
  SUMMERCMS_REF, fast-forward re-runs, refusals with HEAD unchanged,
  summer called from the parent with stdin closed, cd line and PATH hint
- TestScriptShellcheck: shellcheck -s bash new.sh, skipped only without shellcheck
2026-10-08 12:25:24 +02:00
Jakub Zych
392dd3c83f test(14.3-04): cover every summer new branch, key and CLI path
- name rule, existing targets, framework resolution order, inside-checkout refusal
- cleanup on failure, key validation, .env 0600, AGENTS.md symlink
- first commit identity, configured identity, missing git, --no-git, --no-plugin build
- plugin:add relative go.work, plain make:plugin routes, GenerateAppKey, summer new via the CLI
2026-10-08 12:18:14 +02:00
Jakub Zych
cfd110eddb feat(14.3-02): add the sunrise bootstrap script module
- modules/sunrise/new.sh: curl | bash installer wrapped in main(), checks
  Go 1.27+/git/Docker, clones or fast-forwards the framework at the newest
  release tag (SUMMERCMS_REPO/DIR/REF overrides), installs summer and runs
  summer new next to the checkout, prints the cd line and a PATH hint
- sunrise.Script() returns a fresh copy of the embedded script
- smoke, shape, truncation and gated end-to-end tests; README and modules row
2026-10-08 11:51:39 +02:00
Jakub Zych
ca5b2963a5 feat(14.3-01): export lagoon.GenerateAppKey
- key:generate now calls GenerateAppKey; its output is unchanged
- tools can write a fresh app.key without the check-mark decoration
- document lagoon.GenerateAppKey in the lagoon README

Claude-Session: https://claude.ai/code/session_01M8Qu9LgCKYy7iYEho52tW8
2026-10-08 11:41:38 +02:00
Jakub Zych
7bc955a0c2 feat(14.3-01): add summer new with a starter route and relative go.work
- build.NewApp writes sm-<name>-app: go.mod with a relative framework
  replace, go.work, summer.yaml, config/ and a starter plugin
- make:plugin gains an opt-in starter route (GET /{$}) in the same
  routes.go template; plain make:plugin output is unchanged
- plugin:add writes go.work use paths relative to the go.work directory
- bonfire accepts new as a bare tool command name
2026-10-08 11:37:53 +02:00
Jakub Zych
b6b194e5eb fix(boardwalk): rebuild embedded admin shell 2026-10-07 19:07:25 +02:00
Jakub Zych
fa25b4e000 fix(cabana): show to-many relation list columns joined with commas 2026-10-07 18:43:17 +02:00