- caching.md: an https purge_url on another host goes through the site's
proxy; set NUXT_SITE_CACHE_ALLOW_PROXIED=true there, the token is then the
only gate
- cooler README: the purge_url row points at it
- PurgeSiteCacheAfterCommit queues the purge on a per-purger worker after
commit: one request in flight, saves committed meanwhile collapse into
one follow-up, so the admin request never waits for the site
- new cooler.SiteCachePurger.Wait drains the queue; Handle.Close waits for
it at shutdown (timeout plus one second)
- Purge stays the synchronous send; exported names and signatures unchanged
- the purge client's CheckRedirect returns http.ErrUseLastResponse: a 3xx
is logged as a failed purge with its status and the bearer token never
reaches the Location
- BuildRouter logs one warning with the route count and the production
flag when routes name turnstile:open or turnstile:closed and the
verifier has no secret; the secret is never logged
- 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
- LoadCookieConfig refuses a Secure that resolves to false in production,
also when inferred from an http or unset app.url, naming
auth.cookie.secure and app.url
- with supports_credentials, a * origin must be the whole leftmost host
label, patterns must be anchored with ^ and $, and no origin rule may
match the probe origins outside any configured domain
- globally and per profile; non-credentialed configs load as before
- Router.wrap applies the route's profile outside recovery, body limit and
locale, whatever its position in the list or the group nesting
- bufferedResponse.commit merges Vary instead of replacing it
- TestCORSProfileHeadersOnRefusals, TestCORSProfileVaryKeptWithHandlerVary
required by name in the gate
- miniredis v2.40.0 as a test-only dependency (approved at the Task 4 check)
- memory and Redis cache suites, Flush limited to <prefix>cache:
- one parity table through surf.MemoryStore, miniredis and real Redis
- Install, FromApp and site_cache validation; Postgres after-commit purge
- gate: cooler names in the go and docker stages
- 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
- cors_profile_test.go: credentials with * refused (global and profile),
pattern errors, anchored wildcard origins, Vary: Origin on dynamic
answers, api/www credentialed case, unchanged * answers, profile
preflight routes and boot errors, turnstile factory params
- apidocs_test.go: off by default, pinned 5.33.1 page with the RESEARCH
SRI hashes and nonce CSP, ordered compact JSON, provider count, path
validation, escaped title
- store_lookup_test.go: MemoryStore fallback, published Store, no dial
- reorder_hook_test.go (Postgres): hook sees the new order in the tx,
its error rolls back, no hook answers unchanged
- gate: surf names in the go stage, cabana names in the docker stage
- scripts/check-phase19.3.sh: fail-closed gate (go stage, self-test, --all
with NOT RUN docker); a required name that fails, skips or is missing
fails its stage
- cookie_test.go: LoadCookieConfig defaults and errors, TokenCookie
attributes and refresh-window Max-Age, forget cookie, refresh and logout
handlers with a real Refresh and MemoryBlacklist
- turnstile_test.go: open/closed verdict matrix, timeout, form fields,
token sources, body restore, statuses and secret-free logs
- optional ReorderAfterSave(ctx, partition) runs after the new order is
written; an error rolls the reorder back through lifecycleFailure
- reorderPost runs in lagoon.Transaction so lagoon.AfterCommit work a hook
registers (a site-cache purge) runs only after the commit
- pact and cabana READMEs, admin hook table, tree reorder docs
- cooler.SiteCachePurger moved from the Journal plugin: loopback-only http,
32-character token, 2 s detached request, no token in logs or errors
- NewSiteCachePurgerFor names caller keys (for plugin alias keys)
- PurgeSiteCacheAfterCommit through lagoon.AfterCommit, one POST per save
- SiteCacheFromApp for hooks outside serve; Install publishes the purger
unless a plugin already did, and fails boot on an invalid config
- caching docs section, cooler README, configuration row
- 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
- surf: APIDocsProvider (one plugin) supplies a YAML or JSON OpenAPI document,
converted once at boot to compact JSON in source key order; zero or several
providers or a spec that does not convert fail boot
- http.api_docs.enabled/path/title mount GET /api-docs and /api-docs.json
(summercms.apidocs, public, refusing to replace a plugin route)
- the page loads swagger-ui-dist 5.33.1 from jsDelivr with sha384 SRI and
crossorigin on all three assets, a per-request CSP nonce for its inline
script, nosniff and no-referrer
- docs: API documentation page section, README rows, ExampleAPIDocsProvider
- bouncer: Turnstile.Verify posts secret/response/remoteip to siteverify with
a 5 s bound and PHP's open and closed modes (503 unconfigured in production,
422 on a missing, rejected or overlong token, an error or a timeout); errors
never carry the secret or the token
- bouncer: Turnstile.Middleware reads the token from the query, a JSON body or
a form body, restores the body and writes refusals through a published
TurnstileWriter or neutral JSON
- surf: factory turnstile:open|closed from turnstile.secret/turnstile.field
(a published *bouncer.Turnstile wins), client IP with trusted proxies; any
other param fails boot
- docs: Turnstile section, middleware table, README rows
- surf: LoadCORSProfiles reads http.cors_profiles.<name> (http.cors keys
without paths, validated the same way); factory cors:<profile> sets the
profile's headers; an unknown profile fails boot naming plugin and route
- BuildRouter registers OPTIONS <path> (owner surf, recovery only) for every
path whose routes name cors:<profile>; two profiles on one path, an existing
OPTIONS route or overlap with http.cors.paths fail boot
- docs: CORS profiles section, middleware table and README rows
- bouncer: CookieConfig from auth.cookie.* (LoadCookieConfig validates domain,
same_site none without Secure, secure false in production), TokenCookie with
Max-Age = refresh window, ForgetTokenCookie, CookieRefreshHandler and
CookieLogoutHandler with an app-supplied writer
- surf: http.cors refuses credentials with a * origin and bad or PHP-delimited
patterns at boot, turns wildcard origins into anchored patterns, echoes the
exact origin and sends Vary: Origin on every dynamic CORS answer; the
non-credentialed * answer is unchanged
- smoke tests for the www -> api refresh and the refused wildcard config
- Cache: Get, Set, Has, Forget, Remember (degrades on cache errors), Increment, Flush
with per-entry TTLs under <prefix>cache:
- memory driver on otter v2.3.0 bounded by cache.memory.max_entries; Redis driver
with SET PX, INCRBY and a Flush limited to SCAN MATCH <prefix>cache:* plus UNLINK
- Install publishes the Cache (sharing one Redis client with the rate limiter);
FromApp serves jobs and commands without a dial
- new docs/services/caching.md, configuration rows, cooler README
- new module cooler: Install, Handle, OpenRedis (no URL in errors), RateLimitStore
(atomic fixed-window Lua script with MemoryStore semantics, per-instance fallback)
- serve installs cooler and publishes the Redis surf.Store before BuildRouter;
BuildRouter looks up a published Store, route:list keeps the memory store
- config keys http.rate_limit.store and redis.url; go-redis v9.23.0
- cooler README, root table row, surf README and rate-limiting, configuration
and scaling docs
- TestTaglistFieldValidate: both modes, trim, dedupe, empty, null, wrong shapes,
customTags false, required; TestTaglistFieldHydrate: stored NULL and bad text as []
- TestReorderFlatNodes: parent 422, duplicate and zero id 422, missing or extra 409,
no depth or cycle check; TestReorderFlatCompile checks the localized list schema
- TestReorderFlatSmoke/partitioned: flat reorder within one partition on Postgres
- vitest: script markup as text, Backspace, blur, paste, datalist, customTags,
string mode; flat ArrowUp/ArrowDown and the before/after drop zones
- export_test.go wrappers for the taglist save check, hydration and node check
- check-phase18.1.sh requires the new names in the go, admin and postgres stages
- ListSchema.Localize copied only the reorder title and partition, so
GET .../schema/list answered a flat reorder without flat: true
- closes the deferred item from plan 18.1-04; the README and the lists docs
already describe the flag on the list schema
- ReorderView reads flat from the reorder data; ReorderBranch takes a flat
prop and shows only move up and move down
- ArrowRight and ArrowLeft do nothing, an inside drop lands after the row
(tree.ts flatZone, flatMove) and flatten sends parent_id null
- vitest flat cases; docs describe the flat page; rebuilt dist
- config_reorder.yaml without parentColumn on a record with no parent_id
column compiles as a flat (WinterCMS Sortable) reorder; maxDepth above 0
on it fails boot, an explicit parentColumn still means a tree
- ListReorder.Flat and AdminReorderTree.Flat (flat, omitempty); flat nodes
carry parent_id null, a save writes only the sort column, a parent is a
422 on nodes (reorder.flat_parent); 409 and duplicate rules unchanged
- swag descriptions, OpenAPI and schema.d.ts regenerated
- acme.catalog badges reorder fixture, TestReorderFlatCompile and
TestReorderFlatSmoke; docs Flat reorder section and cabana README
- string mode: split on the separator (comma or whitespace), normalised,
stored joined by a comma or one space; null stores an empty string
- customTags false refuses a tag outside options (taglist.not_allowed)
- taglist checked and hydrated on relation forms and settings forms;
settings bind taglist columns; required checked on the tags
- admin: TaglistField string mode and closed option lists, preview renders
the tags read-only as a comma-separated list; rebuilt dist
- docs: Taglist section covers both modes, separator and customTags
- cabana: core type taglist (mode string|array, separator, customTags,
options as a list of tags), column type gate, save-time normalisation
(trim, drop empties, dedupe) and taglist.invalid 422, record hydration
- FormField gains separator and customTags (omitempty); OpenAPI and
schema.d.ts regenerated
- admin: TaglistField chip input with suggestions, registry entry, vitest
- acme.catalog fixture with TestTaglistFieldCompile and TestTaglistFieldSmoke
- docs: forms.md Taglist section, cabana README; rebuilt dist
- money_field_test.go: compile gates (moved from the smoke file and
extended), model column gate, save-time value check, list projection
- settings secret projection and put without a database; tabs, settings
richeditor sanitize and the danger flag as internal unit tests
- Postgres: TestMoneyFieldSave, TestSettingsSecretPostgres and the Phase 18
payload cases of TestPhase10OpenAPIConformance
- vitest: money.ts helpers spec, MoneyField blur-only and server-error cases
- gate arrays and the cabana README Testing section name every test
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
- the settings schema passes the settings model as the DropdownOptionsProvider
- boot refuses a dropdown method on a settings model without DropdownOptions
- cabana README and docs/backend/settings.md describe it
- 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
signpost.Registry.Pattern now takes an item override only when it starts
with a single "/" and holds no surrounding whitespace or control
characters (signpost.ValidItemPattern); a javascript: URL or a
protocol-relative //host falls through to the next layer.
- form-action 'self': injected markup cannot post a native form off-origin
- frame-src 'none': no framed content; a sandboxed srcdoc preview is not
fetched and still renders
- header tests, README and admin-spa docs name the directives
Two first requests to GET .../singleton both missed the record; the
loser's insert hit the unique index and the route answered 500. A
unique violation from SingletonRecord now reruns the transaction once,
which finds the winner's record. TestSingleton fires six concurrent
first requests and requires 200 with the same id from each.
- signpost: activation refusals, host overrides, D-02 pattern layers
incl. PatternFrom, ReferenceOptions depths
- seaglass: removal report, bluemonday bypass corpus, Check
- pact Option.Depth omitempty and the menu item type contracts; party
activates menu item types after Register and before the first Boot
- cabana: repeater compile and decode, dependsOn compile and recheck,
richeditor keys and notices, reorder and singleton boot checks
(Docker-free); repeater save, dependent recheck, richeditor save,
reorder and singleton routes on Postgres; conformance cases for the
documented 404, 409 and 422 answers of the new routes
- admin: richeditor value contract, link popover, notices and chunk
failure; repeater bounds, remove and reorder, collapsed rows;
dependent dropdown states and last request wins; reorder
announcements, partition switch and tree rules; SingletonForm spec
- gate: required names in the go, admin and postgres stages
- scripts/check-phase17.sh: stages go, plugins, admin, layers, docs and
postgres across the framework, four plugin repositories, the host and
both layers; go test -json, vitest JSON and TAP detectors, required
layer test files, --self-test, --all with NOT RUN postgres and
PHASE17_REQUIRE_POSTGRES
- TestRegistryDuplicateKey: a duplicate key fails Activate naming both
plugin IDs, nothing is published
- TestSanitizeByteStable: clean editor HTML comes back byte for byte
- ReorderView: indented tree with drag handle, move up/down, outdent and
indent (buttons and arrow keys on the handle), pointer drag before, after
or inside a row and never into its own subtree; one POST with the whole
node set 400 ms after the last move, flushed on leave; a failed save
restores the saved tree, a 409 reloads; loading, empty, load-failure and
403 states; partition selector
- ListView: outline Reorder button before New record, passing the filtered
partition
- SettingsIndexView opens a singleton controller's record through
GET …/singleton; FormView in singleton mode has no Delete and returns to
the settings index
- reorder.* and messages.list.reorder phrase keys (en, pl), smoke spec, docs
- RepeaterField: rows with drag handle, index, titleFrom title, error chip,
move up/down (buttons, ArrowUp/ArrowDown on the handle, native drag and
drop), expand/collapse, remove (asks only for a row holding a value),
add with nested defaults, maxItems counter, empty state, collapse all;
every change edits form state only
- 422 row paths (content.3.answer) open the row, mark it and focus the
nested control; the banner no longer repeats row messages
- DropdownField dependsOn: waiting, loading, not used, empty, loaded with
depth indent, failed with retry; cached per sibling value, last request
wins, a stale value is cleared after a sibling change and announced
- FormView provides form feedback (errors and notices by row path, the
polite live region, its confirm dialog) and the options cache; FormGrid
provides the sibling scope for repeater rows
- repeater.* and form.dependent_* phrase keys (en, pl), smoke specs, docs
- RichEditorField: Reka toolbar, link popover with the http/https/mailto/tel
and relative URL policy, Edit HTML view, lossy note, skeleton while the
lazily imported TipTap chunk loads, HTML view fallback when it fails
- untouched values are submitted byte-identical; after an edit the value is
the editor HTML (empty paragraph -> ""); a value from outside (the save's
sanitized answer) reloads the editor
- read-only renders through a client display allowlist, never as stored
- TipTap 3.31.4 (core, pm, starter-kit) pinned exact after the package check
- richeditor.* phrase keys (en, pl), smoke specs, forms docs