- 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
- 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
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
- Migrate iterates migrationSets, so the startup summary, --pretend, the
production question, the applied lines and migrate:status --json read
the same list; each set keeps its error wording through a label
- migrate reports pending-before minus pending-after as its applied
lines and result line, also when Migrate fails partway (then it prints
what was applied and returns the error)
- TestMigrateAppliedLines (Docker) pins both (WR-07)
- serve summary lines at -v and -q, records and HideOnTTY, DSN labels
- shutdown report: aborted requests fail the stop, a drained stop is nil
- request log: request_id cap, default on, off by config or env, never the
path for matched, 404 and 405 routes, component http; route:list --json
- WorkerStats through runAttempt: done, failed, in flight, handed back
- queue:clear and lagoon's production question on a pseudo-terminal:
no cancels with exit 1, --force, -n and other environments skip it
- Docker: PendingMigrations, migrate/rollback/deferred:purge --pretend,
production runs off a terminal never read stdin, migrate:status --json
- key:generate --json and -q
- River logs as component river at warn, conga at log.level, -v lets River info through
- an app-published logger wins for River and conga
- GORM: warn by default, info traces at -vvv, record-not-found ignored, parameterized SQL
- gate: the diary, sunscreen, conga and lagoon names in the go stage
- migrate, migrate:rollback and deferred:purge gain --pretend and --force;
they and queue:clear confirm only in production on an interactive
terminal, so deploys over SSH proceed as before
- result lines: Applied N migrations, Rolled back <id> of <plugin>,
Cleared N jobs from <queue>
- --json for migrate:status, key:generate, route:list, admin:token:create,
admin:token:list, admin:token:scopes and websockets:health; -q keeps the
key and the token
- hints for an empty DSN, an unreachable database, an invalid app.key, a
missing uploads bucket and an application binary run below its root
- flare and websockets:health return bonfire.Reported after printing
- long help and examples for every application command, greeter:hello and
blog:publish; cmd/summer TestCommandsDocumented fails on any gap
- queue:work and schedule:run print a startup summary and a shutdown
report on a terminal, log them as records otherwise, and return nil
after a clean stop
- schedule:run --once ends with 'Ran N due entries (M failed)'
- summer dev runs bin/<app> serve with the forwarded global options,
reports through Output, stops the old server before starting the new
one and prints a dev report on Ctrl+C (exit 0)
- lagoon.DatabaseLabel describes a DSN without its secrets
- Long help and examples for queue:work, schedule:run, dev, docs:serve
- serve prints a startup summary on a terminal (version, environment, URL,
admin, database host and name, pending migrations, plugins, drivers) and
logs it as a 'startup summary' record plus 'listening on <addr>'
- on SIGINT/SIGTERM serve drains, prints and logs a shutdown report and
returns nil (exit 0), or the drain errors (exit 1)
- the request log is on by default under diary component http with
request_id; the route pattern, never the path, is logged
- lagoon.PendingMigrations, conga.Worker.Stats, diary.HideOnTTY,
bonfire.IsTerminal and bonfire.Details back the summary and report
- River's logger is diary.For("river") (warn by default), conga's own lines diary.For("conga"); an app-published logger still wins
- GORM logs through logger.NewSlogLogger(diary.For("db")): warn level, record-not-found ignored, parameterized SQL traces only at verbosity 3
- Smoke tests TestRiverLoggerUsesDiarySmoke and TestGormLoggerUsesDiarySmoke
- attach.Store returns attach.ErrProtectedNeedsPrivateBucket for an
is_public false upload while storage.uploads.public_url is set, before
any blob is written
- OpenBucket's public_url line becomes a warning naming the refusal
- manifest_tags_test.go: build.tags load, trim, dedupe and validation
errors naming build.tags; SaveManifest keeps tags and plugin order;
goBuildArgs puts one -tags before -o
- s3_test.go: s3:// refused without summercms_s3 naming the tag, opens
with it (both build modes, any scheme case)
- diskext_test.go: RESEARCH probe rows and table consistency
- url_test.go: public_url base joining, File.URL and thumbnails,
malformed values refused naming the key
- store_test.go: Postgres regression table through Store
- gate: attach names in both tag modes, Store regression in docker
- OpenBucket validates public_url (absolute http/https, host, no user info, query
or fragment; the error names the key) and records it next to public_path_prefix
- a set public_url logs one start-up line: the bucket behind it must hold only
public files, since it bypasses the is_public check
- attach.PublicURLBase; PublicURL, File.URL and thumbnail URLs use the base when
set and keep the public_path_prefix proxy URLs otherwise
- TestPublicURLSmoke; storage Public URL base section, attachments, configuration
and lagoon README
- DiskExtension: a fixed table of specific sniffed types keeps a matching client
extension or uses the canonical one; generic sniffs keep the client extension
unless it is script-capable (then txt or bin); never asks package mime
- Store names the blob with DiskExtension(sniffed, ext); file_name, the allow-list
and the stored content_type are unchanged
- TestStoreDiskExtensionSmoke: a PNG uploaded as x.html is stored and served as .png,
report.docx keeps .docx, an HTML body named note.txt keeps .txt
- attachments docs on disk names, storage deploy note on nosniff and allow-lists,
lagoon README
- Manifest.Tags from build.tags (^[a-z0-9_]+$, duplicates dropped), round-tripped by SaveManifest
- summer build passes -tags=<list> through goBuildArgs and prints the build tags line
- attach: s3blob imported only by s3_on.go (//go:build summercms_s3); an untagged binary
refuses an s3:// bucket_url naming the tag, build.tags and -tags
- go mod tidy adds the AWS SDK v2 modules as indirect requirements
- storage, modules, scaffolding, configuration and scaling docs plus the lagoon README
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.
- 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
- 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
- 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
- surf: a transitive three-route family, HEAD and sorted Allow on family
paths, a family across two plugins with per-member middleware, refused
trailing-slash and multi-segment shapes
- conga: refusal messages name kind and queue, a configured queue counts
as served, a delayed unregistered dispatch waits scheduled
- lagoon: prohibited under a wildcard, on a dotted path and after bail
- tide: quoted, RFC 5987 and multi-date download names; a captured id
beside a masked notification id
- lagoon.ValidateRequest supports Laravel 9 prohibited (!required, not
implicit); with no catalog line its message is validation.prohibited
- tide compares Content-Disposition with real calendar dates masked on both
sides; a different name, an invalid date or a one-sided date still diffs
- tide.NormalizePublications masks a Carbon +00:00 $.data.payload.created_at
and an uncaptured positive integer $.data.payload.id as {{id}}
- the album-date test's outside-album sibling moves off payload.created_at,
which now has its own mask
- READMEs and docs describe the rule and both masks
Keep form save behind in-flight uploads, make retries idempotent via X-Upload-Id, cap remaining JSON bodies, and surface pending pivot type errors instead of zeroing them.
Co-authored-by: Cursor <cursoragent@cursor.com>
- lagoon: Date and TimeOfDay through JSON, text and real DATE/TIME
columns; Fill text fallback without changing earlier conversions;
required on zero dates; deferred_bindings shape, store isolation and
envelope; PurgeDeferred cut-off, after-commit blobs, SKIP LOCKED,
skipped types and the deferred:purge command
- attach: Store limits, extensions, MIME patterns, default lists, key
shape and blob cleanup; IsAllowedImage formats, polyglots, ceiling
- conga and pact: framework purge schedule entry and forged jobs; the
six relation child hook interfaces
- cabana: fileupload and datepicker compile, upload, remove, caption,
reorder and bounds; deferred commit order, rollback, applied-only and
concurrent saves; relation contracts, forms, CRUD, deferral, schema
- Date (DATE) and TimeOfDay (TIME) with Scanner, Valuer, JSON and text forms
- Fill falls back to encoding.TextUnmarshaler for string sources after
every existing conversion, so time.Time and the new types fill from JSON
- required treats a zero time.Time, Date or TimeOfDay as empty
- lagoon README, models and casts-and-validation docs
- deferred_bindings migration set under summercms.deferred with backend_user_id
- lagoon.DeferredBind/Unbind/Bindings/Forget/Slaves scoped by DeferredKey
- lagoon.PurgeDeferred with SKIP LOCKED batches and after-commit blob deletes
- attach.Store with the ported image guard, extension and MIME limits
- attach.Relation, attach.HasRelations, attach.BlobKeys, File.ThumbKey
- lagoon README and attachments docs
PHP 8's (string) cast of a float formats with the precision ini (14
significant digits, %.14G), not the shortest round-trip form: 1/3 is
0.33333333333333, 1e14 is 1.0E+14 and 5e-324 is 4.9406564584125E-324.
phpFloatString, used for the string form of JSON floats in size, in, regex
and integer checks, printed up to 17 digits and switched to the exponent
form only from 1e15. TestPHPFloatStringMatchesPHPCast pins 27 values to
php -r output.
A 162-case truth table recorded from WinterCMS's validator (the vendored
winter/storm Factory, Laravel 9) found three divergences, all on arrays:
- in compared array elements loosely; Laravel uses array_diff, an exact
string comparison, so ["1.0"] is not in 1,2;
- not_in failed when any element was listed; Laravel's validateNotIn is
!validateIn, so an array passes unless every element is listed;
- not_in failed an array without the array rule; Laravel passes it.
validate_rules_test.go keeps the whole table (accepted, array keys,
boolean, numeric, integer, in/not_in, sizes by type, regex, dates and
comparisons, url, presence, nested and map wildcards, bail and order).
A photo whose original is missing, does not decode or declares more than
4096x4096 pixels made attach.File.Thumb return an error, and every listing
that shows the photo answered 500 from then on: one 100-byte PNG uploaded
by any household member broke GET collections and the album for everyone.
Thumb now follows WinterCMS's File::makeThumb catch branch: it logs the
reason at warn level, stores WinterCMS's BrokenImage picture (exported as
attach.BrokenImagePNG) under the thumbnail key and returns its URL. Invalid
arguments, storage errors and encode failures are still errors.
- attach.PublicURL and (*File).URL build Winter File::getPath() URLs; the
thumbnailer decodes webp via golang.org/x/image v0.46.0 and checks the
image size from the header before decoding
- tide requests carry multipart parts (files beside the fixture pinned by
sha256) encoded with the fixed MultipartBoundary, so PHP and Go receive
byte-identical bodies
- tide masks the random partition, disk name and file id of url/thumb_url
upload URLs while still diffing prefix, size, mode and extension, and
NormalizePublications masks Carbon dates in the published album
- lagoon.ValidateRequest ports Laravel 9 request validation: wildcard
expansion, implicit-rule stop, bail, sometimes/nullable/blank skipping,
size messages split by type and character-counted string lengths
- ParseRules, In, CustomRule, UploadedFile and ErrorKeys for rule tables
- pl/en lagoon::validation catalogs ported verbatim from WinterCMS
- lagoon.Validate answers a numeric range failure with the bound that
failed (min, max or numeric between) instead of always max
Add a backend admin migration that creates a unique index on
lower(backend_users.email). Rows copied from WinterCMS may hold emails
that differ only in case, so the migration refuses to run and names the
clashing logins instead of choosing an account to drop.
- lagoon.OrderBy takes variadic lagoon.OrderOption values; lagoon.Collate(name)
emits a validated, double-quoted COLLATE clause (e.g. "pl-x-icu")
- remove the exported CheckLocale and the ICU pl-PL check from Open and Use
- framework test containers and per-test databases are plain PostgreSQL
- lagoon README, root README and docs pages drop the locale requirement;
queries-and-pagination gains a "Sorting with a collation" section
backed by ExampleCollate
- docs/database: models, migrations, queries and pagination, relations,
casts and validation, attachments and transactions (lagoon.Transaction,
lagoon.AfterCommit, nested savepoints, lagoon.OnDatabase)
- docs/services: configuration, events, routing with auth groups, rate
limiting, authentication, the OAuth server, mail and localization
- runnable Examples for lagoon, attach, compass, surf, wire, bouncer,
wristband, postcard, phrasebook and festival; lagoon TestDocs* regions
run on the package's Postgres harness through DocsDB
- 15 new required pages
- lagoon.Transaction doc and README state that a nested call over a root
handle returns an error instead of opening an independent transaction
- beachcomber README no longer promises an immediate sync inside a plain
GORM transaction; it is warned and skipped since 11-08