- make:plugin writes plugins/<vendor>/<name> with module
<app>/plugins/<vendor>/<name>; the package name stays <name>
- make:* and the models-leaf check look in summer.yaml, then
plugins/<vendor>/<name>, then the older plugins/<name>
- summer new puts its starter plugin at plugins/golem15/<name>
- refuse a vendor directory that is a legacy plugin module and a second
copy of a plugin that already sits in plugins/<name>
- help text and docs, with an upgrading note: moving is optional
- nextMigrationFile starts at one second past the newest updates/ timestamp
when that is not earlier than now, so registry order is creation order
- frozen-clock unit test and a registry-order check in the scaffold test
- docs: drop the same-second caveat and the manual order check
- READMEs and service pages name the new pact and wristband identifiers
- an empty MCP scope list resolves to the five fixed names; OIDC discovery is unchanged
Hide removed an owner field from the schema, show and save, but a hidden
relation-manager field's relation routes (panel, link, unlink, child CRUD,
deferred commit) and a hidden partial field's partial stayed reachable.
dropHiddenFields now drops the relation and the partial path too; a partial
the list or preview header or another field still uses keeps rendering.
Admin saves copied the raw context locale into RecordSaved.Locale, which
could be a whole Accept-Language header, while reorder reported a
normalised code. Saves, child writes and reorder now report the primary tag
of the request locale. The pact doc says what Locale is and that per-locale
work keys on RecordChange.Locale, since an admin save writes the row in the
default content locale.
hasMany link and unlink rewrote a child's foreign key (and ClearOnChange
columns) without a RecordSaved, the deferred commit attached session-created
children silently, and unlinking a session-created child deleted it without
the RecordDeleted that should follow its RecordSaved{Created}. These paths
now fire the child's events after the commit; the CRUD service carries the
registry so the deferred commit names the child's controller.
WidgetField assigned its element after append(), so an action the element
dispatched from connectedCallback (inside append) found no target and was
dropped without a result or an error state; widgets that load on connect
never started. The element is now the action target before it is connected.
Rebuilt the embedded admin dist.
The plugin field-type upload route resolved a form extension's field without
checking the extension's Permissions and Visible, so an administrator who
cannot see the extension could still run its Upload. The route now answers
404 for them, like the extension's widget actions and partials.
A required: true field in a form extension became a rule on the owner's
model, where its value never exists, so every save of the host form failed,
also for administrators who cannot see the extension. mergedRules now skips
extension fields; liftExtensionValues checks required on the submitted value
for visible extensions only (create: must be sent and not blank; update:
only when sent).
- ContentType, ContentTypeForController, Records and MatchPath on the
registry; the pure MatchPattern for whole-segment placeholder matching
- two types declaring the same Content.Model or Controller stop the boot
- TestContent* cover lookups, optional Records, boot errors, MatchPattern
and MatchPath precedence (first exact, else first inexact)
- docs: Content descriptors in menu-item-types.md, signpost README
- Delete and each bulk-delete row fire RecordDeleted after commit
- tree reorder fires RecordSaved per moved record (parent/sort in model types)
- relation-manager child create/update/delete fire events naming the
controller that edits the child's model
- TestRecordEvents covers create, update, translated slug, no-op, rollback
(validation, FormBeforeUpdate, extension Save), failing listener, reorder,
child writes, delete and bulk delete; extension Save sees the changes
- docs: Record events in extending.md, admin-controllers notes, cabana README
- admin-controllers: placement, values, Load/Save, permissions,
widgets, partials and boot errors, with the ExtrasPlugin snippet
- Example_formExtension compiles an owner form with the extension
- coming-from-wintercms maps extendFormFields, addFields, addTabFields
and removeField to the form extension
- cabana README feature bullet and Activate row
- pact.AdminActionExtension.Form with AdminFormExtension: a fields
document (fields.yaml format plus before:/after:/zone: and a tabs:
list) given as an AdminFS path or YAML bytes, Load and Save hooks,
AdminFormLoad/Save/SaveResult, AdminFormErrors and zone constants
- cabana compileFormExtensions places extension fields at owner
anchors at boot, regrouping an extended form by tab
- save lifts extension values before any owner path and runs Save in
the record's transaction after the owner row; show merges Load
- TestFormExtensionTracer: c after a on a, b gives a, c, b through the
admin API, stored in the contributor's table
pact.MCPGrant gains BackendUserID, the backend account the consent page
showed. pier gives an OAuth identity a backend subject (on resolution and
after a link event) only while the user's link names that account; a link
to anyone else, or no recorded account, gives none, so a link changed after
the consent never re-targets the grant.
An action whose Applies failed was dropped with no signal, so a section
whose Status succeeded showed a normal card with a button silently
missing. The section is now also sent with error: true, and My account
shows its error line. The error field's description, the cabana README
and the My account docs say so; the OpenAPI document and the SPA schema
types are regenerated.
reserveScript reads the record keys of the binding set's members, which it
cannot declare in KEYS, and its declared keys hash to different slots, so
Redis Cluster refuses it and a key-pattern ACL must allow the whole
<prefix>mcp: range. The script's comment, the pier README's
mcp.sessions.store row (which also no longer says the store opens on the
first request) and docs/services/mcp.md say so.
promptHandler logs a failing Render's error with the prompt name and
answers the fixed JSON-RPC internal error "pier: the prompt could not be
rendered", as dispatch does for tool calls (A-WR-08), so database or
provider text never reaches the agent or its transcript.
TestPromptRenderError renders a prompt that fails with a database error
naming its host and asserts, on both paths, that the client sees only the
fixed text and the cause is in the log.
Fan-out matching snapshots the sessions (and stateless entries) under the
router-wide lock, releases it, and reads each identity through a small
identity box that is never held across I/O, so an event no longer waits on
a session that is re-filtering while it holds sr.mu or sp.mu.
Widening and the fan-out recomputes run their guard checks with
guardContext: not cancelled with the triggering request, but bounded by
mcp.call_timeout, so a provider that never answers cannot hold a session
lock forever. README and docs/services/mcp.md describe the wider meaning of
mcp.call_timeout.
TestLockScopeUnderWidening blocks a widening guard check, fires a
permission event for that session and asserts that another session's call
still finishes, and that a never-answering check ends at call_timeout.
- plain-key shortcuts test composedPath()[0], and a custom element inside a
form counts as editable (closed shadow roots)
- Ctrl/Cmd+S and Ctrl/Cmd+Enter blur the focused field before clicking, so a
pending taglist draft or native change is committed, then refocus it
- safeUrl keeps javascript:/data: URLs out of href and window.open
- a failed contributions GET shows a toast and keeps the sections; a section
with error true shows an error line
- My account confirm and stale texts get their own phrase keys
- rebuilt modules/boardwalk/dist
- an account action URL is kept only when it is http(s) with a host or a
same-origin path; anything else is dropped and logged without the URL
- a failing Status marks its section error: true and a failing Applies
drops only that action, instead of a 500 for the whole list
- status text goes through localizeDetail (B-IN-03); the non-string
payload refusal is the phrase key action_prompt_invalid (B-IN-02)
- the record action annotations and pact's prompt doc say what happens
(B-IN-05 text); admin.json and schema.d.ts regenerated
- pact.MCPSubjectChanged and pier.Event gain TokenID and ClientID; a
narrowed revoked event closes only matching sessions and stateless servers
- cabana fires the admin token revoke with the token row id
- towel.WithClientIP/ClientIP; pier puts the address (surf applies
http.trusted_proxies) on the resolve context and ResolveBackendToken logs it
- pact.FrontendTokenScope and pact.HasFrontendTokenScopes declare member
(usr_) token scopes; the admin token catalog never reads them
- pact.ValidTokenScopeName carries cabana's scope-name pattern; cabana's
g15_ catalog switches to it with unchanged behaviour and messages
- MintAudience stamps auth_time equal to iat; refreshAudience copies the
presented token's value and never renews it (absent stays absent)
- bouncer.ClaimAuthTime and bouncer.VerifyAuthTime for plugins, with the
same signature, audience and expiry checks as VerifyClaims
- TestAuthTimeSmoke traces mint, refresh and read
- pact.MCPPrompt.Writes (additive; the zero value keeps today's listing)
- pier's allowsPrompt hides a prompt declared with Writes from an identity
without write, as it hides a tool that is not read-only (C-IN-01
framework part, P-28)
- a path field must be required in the reflected schema (boot error), and
a missing value, an empty, . or .. segment, or a / inside one segment is
a tool error before dispatch (A-WR-01, A-WR-02)
- a failed guard dispatch logs its cause and answers a fixed tool text;
only an invalid token and an unknown or deactivated account keep their
own fixed texts (A-WR-08)
- path and query scalars decode with UseNumber, so integers above 2^53
keep their digits (A-IN-04)
- the dispatched request carries the outer Host, X-Forwarded-Proto and
X-Forwarded-Host (A-IN-05)
- an embedded struct on a GET or DELETE tool fails boot (A-IN-07)
- a dispatch whose guard list or principal kind contradicts the tool's
guard is refused (A-IN-08)
- Identity.fingerprint covers scopes, both subjects, the ceiling and the
token kind; a stateful lookup or stateless hit whose fingerprint changed
re-filters the server with the event diff under its own mutex (A-WR-04)
- Mount warms a Redis store in the background with a 1 s to 30 s backoff
and every request kicks a pending open without waiting, so stateless-only
instances subscribe to fan-out (A-WR-05)
- a stateless subscriptions/listen POST re-verifies its bearer every
recheck interval; an LRU-evicted stateless server closes its sessions
(A-WR-06)
- resolvePersonalToken refuses a subject with TokenID 0, and the
pact.MCPPersonalTokenResolver contract says one resolver per application,
false means unauthorized and TokenID keys the binding (A-WR-09, A-IN-03)
- cabana wraps the bouncer backend guard middleware: a marker that lists the
backend guard with Backend false answers 401 before the guard honours it,
as ResolveBackend already does, so a frontend user id is never read as an
administrator's by the admin API or a backend-guarded plugin route
- found by plan 19.7-05's TestDispatchMarker (admin API in-process case)
- '?' help dialog (ShortcutHelpDialog) listing the current screen's shortcuts
- Esc = Cancel on record forms; '/' focuses the list search
- settings forms: Save and close (Ctrl/Cmd+Enter, then /settings) and Ctrl/Cmd+S
- My account: Ctrl/Cmd+S
- modal forms: Ctrl/Cmd+S and Ctrl/Cmd+Enter submit the innermost dialog only
- docs: Keyboard shortcuts section in the admin SPA page
- app/hotkeys.ts: dependency-free data-hotkey core (Ctrl/Cmd+S, Ctrl/Cmd+Enter,
Ctrl/Cmd+Shift+S, Esc, / and ?), innermost-modal root, capture-phase modifier
listener, platform labels and the help-dialog state
- Button hotkey prop (data-hotkey, aria-keyshortcuts); shortcuts installed by AppShell
- FormView: save keys and tooltips, Create and new on create forms via remountView
- phrasebook: form.create_and_new and the shortcuts group (en, pl)
- personal tokens only behind mcp.allow_personal_tokens (401 without a
lookup otherwise); an admin token yields a backend-only identity with
its scope limit per dispatched method, a user token a frontend-only one
- read/write ceiling and prompts proven by the tracer's subtests; the
audit line asserted to carry the tool and never the arguments or token
- the mounted *pier.Pier is published on the application (test seams in
export_test.go)
- docs/services/mcp.md with the tested acme.blog example, site.yaml note,
cross-links from authentication.md, registration.md and the surf README
- pact.HasAccountContributions compiled at Activate; GET /account/contributions
and POST /account/contributions/{name}/actions/{action} behind interactiveOnly
(AccountContributionView, AccountContributionActionView, AccountActionResultView)
- RecordAction.prompt (RecordActionPrompt) for a pact.AdminRecordAction with a
Prompt; the record action route accepts payload only for such an action
(trimmed JSON string, non-empty, capped at max_length) and hands it to Run as
Value through CRUDService.RecordActionWithValue; a prompt without a label
fails boot
- fireSubjectChanged: pact.MCPSubjectChanged for revoked tokens, Administrators
saves that changed role, permissions or superuser, Roles saves that changed
grants, deactivation and deletion
- phrases backend::lang.form.action_prompt_required/_too_long (en, pl)
- smoke tests TestAccountContributionsSmoke and TestRecordActionPromptSmoke;
TestRecordActionPromptRun on Postgres; route inventories, CSRF walk and
OpenAPI conformance cases extended for the two routes
- regenerated admin/openapi/admin.json and admin/src/api/schema.d.ts; cabana
README, docs/backend/{admin-controllers,users-and-permissions}.md
- surf.Router.MountStream and recoverStream: a raw, unbuffered route kind
whose http.Flusher reaches the client; panic after the first byte only
aborts the body
- surf.MCPBuildTag and the mountPier hook: mcp.enabled is inert without the
summercms_mcp tag until it is true, then boot fails naming the tag
(pier_off.go is the untagged side)
- wristband parseAuthorizeScopes takes the server's allowed set built from
Options.ScopesSupported; the default set is unchanged
- cooler.KeyPrefix exported for the MCP module's Redis keys
- smoke tests TestStreamingRouteSmoke, TestPierMountSmoke, TestMCPScopesSmoke
- surf, wristband and cooler READMEs, docs/services/{routing,oauth-server}.md
- bouncer.Dispatch, WithDispatch, Dispatched: context-only pre-resolved
principal marker honoured by Registry.Middleware for listed guard names
- cabana.ResolveBackend honours a backend-kind marker; additive
ResolveBackendToken, ScopeLimitFor and BackendPrincipal resolvers
- surf.Router.Handler and compileHandlers: one compiled chain per route,
shared by BuildRouter validation and compile
- smoke tests TestDispatchSmoke, TestDispatchedSmoke, TestDispatchMarkerSmoke
- READMEs and docs/services/{authentication,routing}.md updated
- settingsSectionOf returns the current entry's category group (null for one-entry categories)
- SettingsPanel mirrors the plugin side menu, collapse included
- AppShell shows it on settings forms and settings-linked controllers without own navigation
- docs/backend/settings.md describes moving between settings pages
- cabana, not the contributor, applies guardOutranked before a contributed Run
- A contribution to summercms.cabana.users is 403 user.outranked when the target outranks the caller
Co-authored-by: Cursor <cursoragent@cursor.com>
- fields.yaml trigger (action disable|hide, field, condition checked|unchecked) compiled to FormField.Trigger
- boot errors for malformed triggers, controlling fields that are not a switch or checkbox, and triggers in repeater rows, settings and relation forms
- Administrators permissions overrides are disabled while Superuser is on (D-06)
- SPA: trigger.ts (triggerState, applyTriggers), FORM_TRIGGER_NOTES, the trigger note in FormField, FormView tabs/banner/payload after triggers, PreviewView hide only
- regenerated admin OpenAPI and schema.d.ts; rebuilt modules/boardwalk/dist; docs and README
- cabana.PermissionRecordLock and the optional PermissionRecordLockProvider
- RecordMeta.PermissionLocks (meta.permissionLocks) on show and save responses, notes localized
- save rule: a record-locked code keeps its stored value, a differing present value is 403
- Roles lock code-declared grants (locked_by_code); Administrators lock every override on your own record (locked_self)
- SPA: FORM_PERMISSION_LOCKS, locked codes never sent, record-locked rows in the permission editor
- regenerated admin OpenAPI and schema.d.ts; docs and README
- Settings -> System -> Roles (summercms.cabana.roles, backend.manage_roles,
order 110): name, code slugged from the name, description, checkbox
permission editor with the editor's ceiling
- system roles (is_system or code-declared grants) keep their code and
cannot be deleted; a role any admin holds (deleted ones included) cannot
be deleted, the refusal names the role and the count
- role codes unique among roles
- activate, deactivate and restore as record and bulk actions; delete is
the soft delete through deleted_at, never a hard delete
- deleted admins listed and opened (unscoped), deleted/deactivated row
states, Deleted filter (hide / only), preview status hint
- deactivate and delete bump tokens_valid_after; activate and restore do not
- self delete and self deactivate refused; outranking and last active
superuser guarded on delete, deactivate and bulk rows (whole selection
rolls back)
- create and edit administrators: password (min 4, max 72 bytes, bcrypt
at admin.password.bcrypt_cost), role picker, radio permission overrides,
superuser switch; no mail on create
- superusers hidden from non-superusers in list, lookups and schemas;
is_superuser projected to superusers so an untouched save keeps it
- save-path rules: superuser-only switch, role and permission ceilings,
no outranking, no own role/flag/overrides/password change, last active
superuser not demoted (advisory lock + count in the write tx)
- password set for another admin bumps tokens_valid_after
- role, superuser, activation and last sign-in list filters
- pact.HasAdminActionExtensions / AdminActionExtension: plugins add record
actions to any admin controller by ID, merged at boot before permission
validation (unknown target, no preview, duplicate name stop the boot)
- internal summercms.cabana pseudo-plugin, appended only when some plugin
registers admin controllers: Administrators list and preview, settings
link under System, backend.manage_users and backend.manage_roles with no
role grants
- backend::lang user, user.role and permissions copy (en, pl)
- smoke tests: compile-time seam and settings, Postgres list/preview/action
57a849d made lagoon.Status read migrationSets, which changed its result
for consumers: the framework sets came first and a plugin with no
migrations lost its row. Status is back to its pre-57a849d behaviour.
migrate:status (table and --json) now reads the unexported setStatus,
which lists the framework sets and the plugins from migrationSets, so it
still agrees with migrate --pretend. Both share historyRows.
- TestStatusPluginRows pins Status: plugin rows only, an empty row for a
plugin with no migrations or no history table, none without HasMigrations
- TestMigrateStatusJSON also checks the table lists the framework sets
- TestTwoPluginMigrationSetsIsolated and TestDocsMigrate are back to the
plugin-only rows
RelationContract (hasMany) and FieldRelationContract (belongsTo) accept
ClearOnChange, a list of nullable columns set to NULL whenever the relation
changes the foreign key: a hasMany link and unlink (deferred ones included)
clear them on the child, and a form save that moves a belongsTo key to
another record or to none clears them on the record unless the same save
writes the column through its own field. Boot refuses the option on a
belongsToMany and for a missing, non-nullable, foreign-key or primary-key
column. The zero value changes nothing.
- bonfire.Sanitize (additive), moved from diary's console channel: tab
kept, CR and LF to a space, every other control character dropped
- applied before styling to every error-block line (message,
explanation, Try lines, -v chain), process-view item names and errors,
the current item of spinner and progress, failure lines, and table
headers and cells in both layouts
- diary's console handler calls bonfire.Sanitize; README and the
writing-commands page say so (WR-05)