Importing attach/webp registers the encoder library's WebP decoder ahead
of the pure Go one for the whole process, so WebP uploads are decoded
through the system libwebp when it is installed. The lagoon README and
the attachments page say so and name -tags nodynamic.
- pact: the content kinds and tools, zero facts, the value types
- cabana: mergeRequestOptions precedence, one call per type, fail-open,
no shared maps
- attach: VariantKey on thumbnail names
- attach/webp: every image kind, the stored content type, the
broken-image thumbnail and a refused mode
- attach.VariantKey: a thumbnail's copy in another format next to it
- attach/webp (separate package, so only binaries that import it link
the encoder): Encode at quality 80 and Variant, which writes the WebP
copy of a thumbnail once and leaves no file on a failed encode
- github.com/gen2brain/webp v0.6.4, decided in
.planning/notes/2026-10-28-webp-gen2brain.md
- lagoon README and docs/database/attachments.md
- pl validation.yaml string line is Polish (Winter's pl file leaves it
in English); the header notes the deliberate divergence
- pl validate.yaml min and max use Winter's numeric wording, since those
keys only serve numeric and integer bounds
- min and max compare the characters of a string or a number's decimal
form and the element count of a slice or map on any field without
numeric or integer (Laravel's getSize), with the min.array and
max.array messages for lists
- blank values, booleans, dates and byte slices are not measured; a
malformed bound is an error on every non-numeric field
- reverse the 261104-f4x pin, prove it through the cabana settings save
sequence, and update the README, docs and the storePassword comment
- on a string or url field that is not numeric or integer, min and max
compare utf8.RuneCountInString (Laravel's mb_strlen) and answer with
the lagoon::validation min.string / max.string messages
- a min or max argument that is not a whole number is the
unrecognized-rule error, whatever the value
- rule strings without string or url keep their results (pinned by a
test); between is unchanged
- string requires a string value (pointers and named string types
included), url a URL matching Laravel's pattern (urlPattern, shared
with lagoon.ValidateRequest); both skip an empty or blank value
- lagoon::validate.string and .url in the en and pl catalogs, English
fallbacks for a nil translator
- settings saves whose model rules use nullable|string|max:255 or
nullable|url|max:500 no longer fail with an unrecognized-rule error
- pact.FormFieldType gains RequestOptions, called with the application's
database once per form schema request for each type the form uses
- the form schema route merges the answer into a copy of each field's
typeOptions; a fields.yaml key wins, the compiled schema never changes
- an error logs one warning naming the type and serves the fields.yaml
options only
- TestFieldTypeRequestOptions covers every screen, the precedence, the
call count, the error path, unchanged schemas, form extension fields
and the authenticated handler; a runnable example backs the docs
- the typeOptions description, the regenerated OpenAPI document and
schema.d.ts, the field types page and the pact and cabana READMEs
document it
- pact.ContentDescriptor gains Kind and Tools with the ContentKind and
ContentTool constants
- pact.ContentRecord gains PublishedAt, Parents, Authors and TopHeading;
pact.ContentLocale gains Images, Videos and Headings, with the value
types ContentRef, ContentPerson, ContentImage, ContentVideo and
ContentHeading; every field is optional
- TestContentDescriptorFields proves signpost hands them through and
that a type setting none of them describes as before
- the runnable content example, the pact and signpost READMEs and the
menu item types page document the fields
A plugin developed inside an application replaces its required plugins
with sibling directories (../user). Checked out on its own, that
directory is missing and plugin.yaml lost the minimum (golem15.user: ""),
so the manifest differed between the two layouts and TestPluginManifest
could not pass in both. Such a module is now fetched at the version
go.mod requires, as a consumer would see it.
- listedModule.fallback marks a local replace whose directory is absent
- fallback modules download separately, so one unfetchable version does
not hold up the others
- TestDeriveMissingReplace over a file:// proxy (offline)
The smoke fixture's acme.blog has v0.1.1 for framework v0.6.0 under a
newest release that needs v0.7.0; plugin:install picks v0.1.1, so the
marketplace now marks it compatible.
With a framework version that is not a release (a devel build, an empty
FrameworkVersion) every plugin with a minimum was marked as needing a
newer framework, and Compatible looked only at the newest release, so a
plugin whose older release fits was marked although plugin:install
would install that release. An unknown framework now gives no marker
(and no needs-framework note on the Installed tab), and a plugin whose
newest release is too new is looked up (cached, four at a time) and
counts as compatible when an older release fits.
pluginCatalog.lists was keyed by every distinct query and category and
never dropped an expired entry, so searching distinct strings grew the
process without limit (each entry can hold a 4 MiB answer). Storing a
search into a full cache now drops the expired searches and then the
ones that expire first, keeping at most 256.
plugin:update dropped a go.mod require that was not a strict vX.Y.Z and
treated it as v0.0.0, so a module at a commit after v0.4.0
(v0.4.1-0.<ts>-<hash>) was moved back to v0.4.0. kiosk.Newer now places
a release against a release, a git describe string or a pseudo-version
(by the tag it builds on, in Go's version order); plugin:update and the
admin Plugins screen badge share it. A pseudo-version with no tag before
its commit is left alone with a note naming go get <module>@<tag>.
- TestPluginsEndpoints: 401/403/200 for both endpoints, the update rule
table, cache hit and expiry on an injected clock, failure caching, at
most 4 version lookups at once, call timeout, marketplace validation,
markers, https-only icons, disabled registry making no call
- fix: marketplace categories come only from plugins the screen offers,
not from registry entries dropped for an invalid ID
- check-phase34.sh: TestPluginsEndpoints and useSettings.test.ts required
- GET /plugins looks up each versioned plugin's releases through
kiosk.Client and applies plugin:update's rule (NewestCompatible above
the BaseVersion), with the update command and the framework a newer
release needs
- GET /plugins/marketplace forwards q and category (validated like the
registry), marks installed and incompatible plugins and returns the
install command; ids outside the plugin ID shape are dropped
- 4 s per-call timeout, at most 4 lookups at once, in-process cache
(searches 5 min, releases 15 min, failures 1 min); a failing registry
gives status unavailable, an empty plugins.registry_url sends nothing
- SPA: update badges and Marketplace tab with copy buttons (clipboard
fallback), hidden when the registry is disabled; no install button
- OpenAPI, SPA types, inventories, conformance case, en/pl strings, docs
- GET /plugins (backend.view_plugins, superusers always pass) lists the
plugins from party.Installed, or party.Activated IDs for older builds,
with the built framework version and whether the registry is configured
- Settings -> System -> Plugins entry with the new SettingsEntry.path,
added only for principals allowed to open it
- PluginsView at /system/plugins with an Installed tab (reka-ui Tabs);
settings panel, rail, breadcrumbs and page title know the route
- OpenAPI and SPA types regenerated; contract, token-class and
conformance inventories list the route; en/pl strings; docs
- module installs move to the newest compatible registry release with
one go get; submodules fetch tags, check out the release tag and
require it in go.mod; go.work overrides and version pins are refreshed
- unlisted submodules (git URL installs) update from their own tags and
plugin.yaml; --offline restricts to that
- without an ID every plugin that resolves is updated; in-app, unlisted
and incompatible ones are reported; failure only when a resolved
update cannot be carried out
- same prompt rule as plugin:install
- kiosk.Client reads the registry API v1 over fetchguard (trusted mode,
4 MiB, 10 s, no redirects); kiosk.RegistryURL reads plugins.registry_url
(default https://summercms.io, empty disables)
- build.PlanInstall/ApplyInstall pick the newest release the effective
framework satisfies (or a pin), validate registry strings, go get it and
append it to summer.yaml
- summer.yaml edits (plugin:add and plugin:install) keep comments and
blank lines
- TestInstallSmoke/module installs from a file:// GOPROXY and builds
- party.Info, party.Installed, party.Describe (additive; Plugin and Activate unchanged)
- summer build resolves plugin dirs with one go list, reads plugin.yaml, versions per D-07 (module version, git describe without --dirty, none for in-app plugins)
- build.EffectiveFramework: newest tag of a locally replaced or used framework checkout, else the go.mod require
- generated main calls party.Describe(app, PluginInfo, FrameworkVersion) before Activate; metadata failures warn and fall back to IDs
- summer new goldens and examples/hello regenerated
- kiosk.ReadIdentity: ID()/Requires() as literals or constants across packages and modules, read with go/parser, never run
- plugin minimums from go.mod requires; v0.0.0 is an error, a pseudo-version warns
- kiosk.Validate, kiosk.CheckT (no testing import)
- plugin:add reads IDs through kiosk.ReadIdentity, so const-ID plugins register
- helpOnly also counts no command word, list, help, cobra's shell
completion requests and --help=<bool>, read the way pflag does
- TestHelpOnlyAgreesWithCobra pins the classification against bonfire's
real dispatch, with the passthrough --help case as the exception
- console and logging docs describe which invocations boot quietly
- Options.Args holds the command-line arguments; FromGlobals fills it
from os.Args[1:], so the generated main needs no change
- Bootstrap and Setup install the setup with Quiet when Args only ask
for help (-h, --help) and no -v is given; files keep their levels
- TestBootQuietSmoke runs the generated main's sequence with a plugin
that logs from Register and Boot
- users form partial on the Administrators tab of the role update form,
served by rolesAdmin.PartialData through scopeAdministrators (D-03)
- deleted and deactivated holders marked, superusers only for superuser
editors, first 100 rows with a count note, preview links for editors
holding backend.manage_users
- en/pl phrases, README and docs, TestRolesSmoke asserts the partial
- boardwalk.BuildID hashes the dist tree plus the binary's build info once
per handler; X-Summer-Build on every response, {prefix}/build.json and the
summer-admin-build meta carry it
- the SPA reads the booted build, re-checks on focus, visibility and every
5 minutes, and shows UpdateBanner on a different build
- the route's private inline list and its helper are gone; the
disposition writer asks attach.IsInlineContentType, the same
definition attach.Store uses for the disposition it stores
- headers are unchanged (TestUploadsResponseHeaders, TestUploadsMount)
- surf README and the storage page name the shared function
- attach.IsInlineContentType: the shared inline raster list (JPEG, PNG,
GIF, WebP, AVIF), parameters and case ignored, false for empty or
unparsable values
- writeBlob stores Content-Disposition: attachment for every other type,
so a public bucket or CDN makes the browser download HTML, SVG, PDF or
plain text; content_type and thumbnails are unchanged
- driver tests on memblob and fileblob, Postgres-backed Store assertion,
lagoon README and the storage and attachments pages
- runAction writes "cabana: admin action run" with controller, action,
admin_id and kind (toolbar or field; field name and record id for a
widget), never values, payload, answer or record contents
- the failure and data-rejected lines carry the same keys (admin_id, kind)
- actionAdminID helper shared with the bulk and record lines (unchanged)
- TestAdminActionRunLog pins the line on the acme.demo fixture (Postgres)
- README and the permissions docs describe the run log
- flushStatementAfterCommit marks the statement; AfterCommit logs a warning
when work is buffered on a flushed statement that no longer holds GORM's
transaction. Log-only: the work still never runs, the write succeeds
- subtests pin the late callback warning, a callback between commit and
flush, and a reused chained statement staying silent
- README OnDatabase example registers After(gorm:create).Before(commit),
matching docs/database/transactions.md
- RegisterJoinTable: pivot-ordered reads join the pivot table explicitly;
a Preload cannot be ordered by a pivot column
- TestPNGScriptChunkNaming (no Docker) and TestStoreScriptChunkPNG (Postgres)
pin the todo exploit: a decodable PNG with a script tEXt chunk uploaded in
image mode as x.html is stored and served as .png, bytes unchanged
- storage deploy note separates static servers (nosniff, nginx extension
allow-list, 404 otherwise) from public buckets, which send the stored
sniffed Content-Type; attachments.md points to it
- make:admin-controller edits the singular of its name (--model overrides):
ModelName, modelClass and the YAML under models/<model>/
- NewRecord returns &models.<Model>{} when the model exists; otherwise the
nil body stays and the command says to run make:model
- config_list.yaml offers create and delete with showCheckboxes
- registry.gen.go embeds controllers/*/*.yaml and models/*/*.yaml once they
exist and exposes generatedAdminFS(); scaffolded plugins implement
pact.AdminAssets through it, and a plugin without AdminFS gets a note
- blog example moves its YAML to models/post; docs and help text follow