# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). ## 0.5.19 - 2026-07-17 ### Added * The checkbox migration warning now also covers the un-tick direction: markup carrying the `checked` attribute plus a `value` (or `data-value`) reading false or empty — which used to render unticked and now stays ticked — is flagged once per field, with the same legacy opt-in pointer as the existing warning * Routes can now require an authenticated session. Declare `"requireAuth": true` in a routing.json rule's `param` block and the framework answers unauthenticated requests before the controller action ever runs: a 401 by default, or — for a browser navigation, when `settings.json > auth.loginRoute` names a rule or a path — a non-cacheable redirect to the login page, with the original request snapshotted first so the login action can replay it via `self.resumeRequest()`. A request counts as authenticated when `req.session.user` is set; populating it at login stays the application's job. XHR requests always get the 401, never a redirect. Routes that declare nothing behave exactly as before. Author mistakes now refuse to boot rather than silently leaving a route open: a non-boolean `requireAuth`, or an `auth.loginRoute` the bundle does not declare. * image:list and image:rm — inspect and remove OCI images on the container host image:build targets. Both resolve the host with image:build's own precedence (GINA_CONTAINER_HOST env override, then native buildah, then the container.host setting), so image:list always shows what image:build would build onto. image:list prints an aligned table or --format=json ({ host, images: [{ ref, id, size, created, sizeBytes, createdAt }] } — createdAt is the exact RFC3339 creation time and sizeBytes an approximate machine-sortable byte count derived from buildah's humanized size); an untagged image reports : and a multi-tagged image yields one entry per tag. image:rm takes an explicit image reference or id (--force removes one a container still references; removing one tag of a multi-tagged image untags it — the image survives under its other tags); there is no bulk delete, and the reference is charset-gated before it reaches the host * Role-based route authorization — a route can now require the session user to hold one of a set of roles: routing.json `param.roles: ["admin", "editor"]` (ANY-of match against `req.session.user.roles`; implies `requireAuth`, so unauthenticated callers still get the 401/login bounce). Authenticated callers holding none of the listed roles get a 403 whose body stays generic — the required roles are never echoed to the wire. Declared but invalid `roles` shapes (null, a bare string, an empty array, non-string members) refuse to boot instead of leaving the route silently ungated, and the client-served routing maps no longer ship the authorization keys (`requireAuth`/`roles`/`policy`). * A policy-function escape hatch for route authorization — a route can now delegate its access decision to a named per-bundle function for the record/attribute checks roles cannot express (ownership and the like): routing.json `param.policy: "ownsInvoice"` loads `/policies/ownsInvoice.js` (`module.exports = function (user, req) { return boolean; }`), registered at boot and AND-composed after roles (implies `requireAuth`; evaluation order authN → roles → policy). A policy is allowed only when it returns a literal `true`; any other return denies, and a policy that throws denies with a 403 rather than erroring the response. The 403 stays generic — the policy name is never echoed to the wire. A missing, broken, non-function, non-string or `async function` policy refuses to boot instead of leaving the route silently ungated. Also adds the imperative `self.hasRole(role)` controller helper for actions that authorize mid-logic. * An audit trail — a user-attributed, append-only record of "who did what to which record when", distinct from application logging (it has its own store and never rides the logger sinks). Opt in with `settings.json > audit.enabled: true` and call `self.audit(action, data[, cb])` from an action: `self.audit("invoice.delete", { resource: id })` writes a v1 record `{ id, ts, requestId, actor: {key, roles}, action, resource?, meta?, ip, rule, method, bundle, env }`. The default backend is an append-only JSONL file at `/logs/audit--.jsonl` (per-bundle-per-env, resolved path logged at boot; override with `audit.file`, or point `audit.store` at a connectors.json entry — no connector ships an audit-store implementation yet, so that path currently refuses the boot rather than failing silently). Writes are serialized, so on-disk order matches emit order. The actor is a snapshot of `session.user[audit.actorKey]` (default `"id"`) plus a copy of `user.roles` — never the whole user object; `ip` is the socket address and `X-Forwarded-For` is never trusted. Route authorization denials are recorded automatically when the trail is on (`authz.denied` with `meta.outcome` covering `401` / `login-bounce` / `403-roles` / `403-policy`; opt out with `audit.events.authz: false`), and an audit failure can never change an authorization outcome. Every request now carries an always-on request id, so audit records and JSON log lines correlate by construction (that id honours a sanitized inbound `X-Request-Id`, so it is a correlation key, not attribution — attribution is the actor fields). Malformed `audit` settings — a non-boolean `enabled`, an empty `file`/`store`/`actorKey`, a non-boolean `events.authz`, or `store` and `file` together — refuse to boot instead of leaving a compliance control silently OFF. * `gina image:run ` runs an image on the same container host `image:build` targets, with podman. Detached by default (the container id is printed on stdout, alone, so it can be captured), publishing the port the image EXPOSEs mapped same:same — that EXPOSE is the port the gina-init allocator computed at build time, so it is the one the bundle actually binds. `--publish=:[,...]` overrides it, `--publish=none` publishes nothing. Also `--name`, `--rm`, `--stream` (foreground, mirroring the container output as NDJSON frames) and `--format=json`. Runtime environment comes from `--env-var=KEY=VALUE` (repeatable) and `--env-file=`, and reaches the container without ever entering argv or a shell, so values stay out of the host process list — which is how a ${secret:KEY} placeholder baked verbatim into the image gets its value at start. buildah builds images but cannot run them, so a build-only host (buildah present, podman absent) says so honestly instead of failing opaquely. Host resolution is identical to image:build / image:list / image:rm, and no project is required. * A new `container:` command group — `gina container:ps [--all] [--format=json]` lists the containers on the container host `image:build` targets (running only by default; every container on the host is shown, as there is no gina-specific marker to filter on), and `gina container:stop [--time=] [--force] [--format=json]` stops one. The verbs live in their own group because they act on containers rather than images — the split podman and docker make — but they resolve the host through exactly the same precedence as the image group, so container:ps always lists the host image:build builds onto and image:run runs on. container:stop reports the rung the container came down on, which podman own exit code hides: podman exits 0 whether the container handled SIGTERM or had to be SIGKILLed after the grace period, so a post-stop inspect reads the container exit code — 137 is 128+SIGKILL (it did not handle the signal in time) and anything else means it came down on its own terms. Stopping an already-exited container is a success no-op, so the verb is safe to re-run. Both verbs drive podman and report a build-only host (buildah present, podman absent) honestly rather than as an opaque exec failure. ### Changed * Request data crossing a redirect() now rides the session (the hidden inheritedData flash channel, one-shot — consumed by the next routed GET, gone on refresh) whenever a live session exists; the clear-text ?inheritedData= URL form and its 2000-char cap now apply only to session-less bundles. resumeRequest()'s plain-XHR and non-XHR GET replays no longer drop the halted request's extra data. For a session-ful bundle that halts a credential-bearing POST (a registration or login paused mid-flight), this removes a plaintext-secret-in-URL disclosure: the payload no longer lands in the address bar, browser history, or access logs. ### Fixed * Calling `gina.emit('error', ...)` on the module object no longer throws — `emit` is now an inert stub that always returns `false` (the detached-copy assignment it replaces never dispatched to any listener; the module object is not an event surface — application events go through the controller's `self.emitEvent()`) * image:build: images for projects that declare gina among their own dependencies no longer fail at gina-init with EACCES — the synthesized Containerfile re-hands the runtime home back after the project-dependency install, whose framework postinstall was re-seeding $HOME/.gina root-owned after the earlier hand-back (dependency-free projects were unaffected) * image:build: the pinned framework now actually wins require('gina') inside images built for gina-dependent projects — the node_modules/gina link supersedes the project-extracted copy (ln -sfn against a real directory exits 0 but nests the symlink inside it, so the project's own gina was silently used at runtime while the CLI binaries ran from the global pin) * getRoute() no longer throws (surfacing as a 500 at the caller) when composing a URL for a GET route that declares no requirements block and receives extra params — the deep-link-before-login replay (resumeRequest()) failed on any halted requirements-less GET route carrying data, and the same crash was reachable from the url template filter / getUrl() family, in the browser bundle too. Extra params now land on the composed URL as query parameters. * The relative-path `self.redirect('/path')` form resolves its target again server-side: the route matcher is async and the historical un-awaited call could never match. `redirect()` is now async as a result — prefer `return self.redirect(...)` from controller actions so a resolution error reaches the framework's handler. * A redirect to an unresolvable target no longer crashes the process — the request now gets a clean 404, and the response-header composer tolerates a falsy routing state instead of throwing from inside the error path itself. * Prometheus metrics (app.json metrics.enabled) no longer double-count requests under the isaac engine: the request-lifecycle hook ran at both dispatch layers for any request reaching the router, so counters were double-incremented and the duration histogram double-observed. The hook now records exactly once on either engine, request durations are measured from engine entry, and the dev Inspector Flow timeline keeps its accurate request-start time. ## 0.5.18 - 2026-07-16 ### Added * Bundle-wide `sliding` and `maxAge` cache defaults: `server.cache` now supplies `sliding` and `maxAge` fallbacks that a route inherits when it omits them, mirroring the existing `ttl` fallback — so sliding-window caching can be enabled for a whole bundle from one place. Per-route values still win (an explicit `sliding: false` overrides a bundle-wide `true`), and behaviour is unchanged for bundles that do not set these keys. Applies to cached HTML (swig, nunjucks) and JSON responses. * Pluggable render/output-cache backend strategy: `server.cache.type` sets the default cache backend (`memory` or `fs`) for a whole bundle, and a route with `cache` set but no `type` now inherits it — mirroring the existing `ttl` / `sliding` / `maxAge` fallbacks. A per-route `cache.type` still wins. New bundles default to `memory`; behaviour is unchanged for existing installs (a route with no effective `type` is not cached, exactly as before). The memory and fs backends now share one strategy dispatcher (`lib/render-cache`) that the render controllers and the server read path go through, providing the seam for additional backends. * Added a native schema/DTO builder (gina.dto): author a data shape once and project it to dialect-aware JSON Schema (.toJsonSchema, draft-07 or 2020-12), the live form-validator rules-object (.toRules), and a stable name for the type generator. Also adds a routing-introspect helper that un-collapses inline validator:: requirements into real JSON Schema. Foundation for the #DTO validation pipe and richer OpenAPI/MCP request/response schemas. * Added a POST /_gina/cache/clear admin endpoint (both server engines, loopback-gated like /_gina/cache/stats) that flushes the rendered-output cache — the static: (HTML) and data: (JSON) namespaces, with an optional ?bundle= filter — without ever touching compiled templates or HTTP/2 sessions. It is backed by a namespace-scoped renderCache.clear(bundle) (which runs each entry cleanup so fs bodies + their .meta sidecars are removed) and a new keys() accessor on the in-process cache primitive. A new gina cache:clear [] @ CLI drives it — an offline reclaim of the on-disk render-output dirs for the bundle (including orphaned prior-release dirs, preserving the config/swig infra caches) plus the in-heap flush via the endpoint — with --dry-run and --format=json output. * `bundle:openapi` and `bundle:mcp` now emit real request/response schemas from a route's `param.dto` / `param.responseDto`. A bundle DTO module at `dtos/.js` — a factory `module.exports = function (dto) { return dto.object({ ... }); }` — projects to an OpenAPI 3.1 requestBody plus a 422 response, or an MCP tool inputSchema.body plus outputSchema. An inline `validator::{ ... }` routing requirement is also un-collapsed into a real parameter schema instead of the previous `.*`. * Default-on request validation from a route DTO: a routing.json rule declaring `param.dto` now has its payload validated against that DTO before the controller action runs, answering a clean 422 (field -> rule -> message, localised) on failure and handing the action a type-coerced payload on success. `param.responseDto` now also shapes the outgoing JSON, so a field marked `.exclude()` never reaches the wire. DTOs are resolved and registered at bundle boot, so a missing or un-evaluable DTO fails the boot instead of silently disabling validation. Routes that declare no DTO are unaffected. * Evict by event from outside the bundle: `POST /_gina/cache/clear?event=` (both engines) and `gina cache:clear --event=`. A controller's `self.cache.invalidateByEvent()` only reaches its own process, so this is how one bundle's write invalidates another bundle's cached pages. Both report the number of entries evicted. Note `?event=` previously went unread, so passing it flushed EVERY bundle's output cache instead of evicting that event's entries; it is now honoured and takes precedence over `?bundle=`. * `gina bundle:types @` emits TypeScript declarations from a bundle's DTOs — the fourth projection of a DTO, alongside runtime validation, response shaping and the OpenAPI / MCP schemas, so a shape authored once is also the shape your editor and `tsc` see. Written to `/dtos/index.d.ts` by default (`--output` overrides). Each DTO emits TWO types, because its declared shape and the shape the server actually holds are not the same: `` is what the client sends (and what the OpenAPI schema documents), while `Projected` is what the action reads on `req.dto` and what a `param.responseDto` puts on the wire — with the `.exclude()`d fields dropped. Value bounds (`.min()` / `.max()`) are carried as documentation only, and a `date` field is typed `string` rather than `Date`, because that is what the validator actually hands the action. * Per-form legacy opt-in `data-gina-form-checkbox-value-as-state="true"` (deprecated, transitional) preserving the pre-fix value-driven checkbox ticking while markup migrates to the `checked` attribute; a once-per-field console warning now flags checkboxes whose `value` used to imply the checked state * Render cache: a redis-backed L2 tier — rendered output is cached in a shared redis so multiple replicas serve the same rendered page, and a freshly-started replica serves content a peer already rendered (cross-replica cold-start). Enable it with a bundle-wide default in settings.json — `cache: { type: "redis", store: "" }`, where `store` names a `connectors.json` redis connector — or per route via `cache: { type: "redis", ttl: }`. Writes go to the in-process L1 synchronously and to redis fire-and-forget (the response never waits on redis); a request that misses L1 warms it back from redis with the authoritative remaining PTTL and re-registers the route's invalidateOnEvents; invalidation (delete / clear / invalidateByEvent) propagates to redis; a down or failing redis degrades to per-replica caching (fail-open) with a once-per-process diagnostic warning. The bundle validates its cache configuration at boot and refuses to start on an unsupported shape (redis with `sliding`, a redis route with neither a ttl nor invalidateOnEvents, or a redis strategy with no `store`) — give every redis route a ttl (or invalidateOnEvents), since a non-expiring key is orphaned on a release-namespace change. * bundle:build and project:build now stamp a source-tree fingerprint (fingerprint, builtAt, fpSpec) into the project manifest release records — the substrate for stale built-release detection on local production rehearsals (releaseWatch groundwork) * Two-tier render-cache observability: every cache hit's Cache-Status header now carries an RFC 9211 detail parameter naming the physical tier that served the bytes — detail=memory for the in-process L1, detail=redis for a shared-L2 warm (the cross-replica cold-start visible per request), detail=fs for a disk read-back after a restart — and /_gina/cache/stats gains an additive l2 block on both engines reporting the redis store connection health (status, mode, key prefix, connection-error count and last error), with no network round-trip on the admin path. * releaseWatch — opt-in stale built-release watch for local production rehearsals (#RWATCH): `server.releaseWatch` in settings.json arms a source-tree fingerprint watch on local-scope non-dev bundles; staleness surfaces on `GET /_gina/release/status` and the `GET /_gina/release/events` SSE stream, and `POST /_gina/release/rebuild` rebuilds with an idle-gated restart honouring in-flight requests and `gina.registerBusyProbe` application probes (notify default, opt-in auto mode) * releaseWatch — a live stale-release banner (#RWATCH): with `server.releaseWatch` enabled on a local-scope non-dev bundle, rendered HTML pages carry a self-contained bottom-bar banner that surfaces build/restart staleness live over the `/_gina/release/events` SSE stream and offers a one-click rebuild-and-reload (idle-gated restart for server-code changes, build-only for static assets, contextual force-restart while waiting on idle); shadow-DOM isolated, CSP-nonce aware, injected server-side into both the swig and nunjucks render paths, and byte-inert on every other scope/env * releaseWatch — a `server.releaseWatch.restartMode` option (#RWATCH): `daemon` (default) drains and spawns `gina bundle:restart`; `supervisor` drains and then exits(0) so an external orchestrator respawns the process on the rebuilt release — the daemonless `gina-container` launch path, which has no framework daemon to spawn a restart (requires a restart-on-clean-exit policy, e.g. a k8s Deployment or `docker run --restart=always`). It is an explicit operator opt-in, never auto-detected: a bundle is spawned detached and legitimately outlives its daemon, so probing for a daemon would risk exiting a healthy process after a routine `gina stop` ### Changed * Release-namespaced render/output-cache keys — a framework upgrade (or a per-deploy `GINA_CACHE_NAMESPACE` bump) now auto-invalidates the render/output cache, so a new build never serves a page cached by old code. The cache key AND the `fs` cache path carry a release namespace: `GINA_CACHE_NAMESPACE` (an operator-set id such as a git SHA or build number) when set, otherwise the framework version (`GINA_VERSION`, which is set at runtime). The key format is centralized in ONE builder shared by the three render controllers and the server read path, so the writer and reader keys can never drift. `fs`-cached files now live under a `` directory (`///…`); files from a superseded namespace are orphaned on disk until they are cleared (a cross-strategy flush is a follow-up). Dev mode is unaffected (it runs cacheless). * The Cache-Status miss form is now the RFC 9211 grammar gina-cache; fwd=uri-miss (previously the bare gina-cache; uri-miss, which read as an unregistered parameter to RFC-aware tooling), and the engine-agnostic read path now emits it on a genuine both-tier miss — giving Express bundles their first cache-miss signal on the wire. ### Fixed * A framework error raised from a detached context — a scheduled cron/timer, a worker, or a bootstrap-time `getLib()` call — no longer crashes the process with `TypeError: next is not a function`. The internal `throwError` helper treated the per-request router context (stored globally and never cleared) as if a live request were in flight and invoked a stale, non-callable `next()`, which additionally masked the original error. It now writes an error response only when a live response is still writable, hands back to the middleware chain only when `next` is genuinely callable, and otherwise surfaces the real error (emerg-logged for fatal calls, thrown otherwise). Live-request error handling is unchanged. * The `fs` render-cache backend now survives a server restart. Previously an `fs`-cached response written to disk was orphaned on the next boot (the in-process index starts empty), so it was never served again and its file was never cleaned up — `fs` behaved no better than `memory` across a restart. The server read path now falls back to disk on an index miss, replaying the entry from a sibling `.meta` JSON sidecar and preserving the ORIGINAL absolute expiry: a restart never extends a TTL (a non-sliding entry still expires at its original creation time plus `ttl`; a sliding entry keeps its absolute `maxAge` ceiling). The `memory` backend stays volatile (cleared on restart), unchanged. * getLib() and getConfig() no longer crash with an opaque "Cannot read properties of undefined (reading 'conf')" when the framework configuration build aborts partway (for example a fail-closed secret resolution on an unset ${secret:KEY}) and bundle code, a cron, a worker, or module-load-time bootstrap reads configuration before the build has finished. The resolvers now fall back to the equivalent per-bundle/env configuration when the fully-built container is not yet available, or fail cleanly, so the real underlying boot error surfaces instead of a masking crash. * Event-driven cache invalidation now works. A route's `cache.invalidateOnEvents` was registered but nothing ever fired it, so the documented `self.cache.invalidateByEvent()` did not exist and configured routes served stale content until their TTL expired. `self.cache` now exists on the controller and returns the number of entries evicted. Three further defects in the same path are fixed: re-registering a key that carries a querystring THREW (the registry ran cache keys through a condition evaluator, and `?`/`=` parse as operator tokens) — under the swig engine that throw unwound to the top-level render error handler, which answered 500 and discarded a page that had already rendered; registrations were never reclaimed, so the registry grew one row per cache-miss re-render; and an `fs`-cached entry read back after a restart came back with no registration at all, so firing its event silently failed to evict it. * A failed cache write no longer discards a page that rendered correctly. Under the swig engine a cache-write error unwound to the top-level render error handler, which answered 500 — throwing away a perfectly good response. It now degrades exactly as the nunjucks and JSON renderers already did: the response is served, the failure is logged, and the next cache miss retries the write. * A checkbox's `value` attribute no longer decides its `checked` state: the HTML `checked` attribute alone drives the initial rendering, and the posted boolean is derived from the live checked state instead of the value string — so a consent-style checkbox rendered with `value="true"` is no longer silently pre-ticked, `value="false"` no longer un-ticks a server-checked box, a checkbox unticked from consumer script no longer posts `true` off its stale value, and a neutral-valued checkbox with an `isBoolean` rule posts real booleans instead of going dead (#49) * The published TypeScript declarations now describe the runtime: the main entry declares a value (previously 'import gina from "gina"; gina.lib' failed to typecheck for every consumer), the module surface carries all 78 runtime members including 'gina.dto', 'SuperController' gains its i18n / jobs / trailers / events / template-override methods plus 'GinaRequest' and 'req.dto' typing for route DTOs, fictional declarations were removed, and a two-part gate (a consumer fixture compiled by tsc through the package exports map + a runtime-parity test) keeps the declarations and the runtime in sync. * Restored error diagnostics that a bitwise `|` was silently destroying at nine sites across the HTTP server, the browser client bundle, and three CLI commands. `err.stack|err.message|err` coerced the Error to the number `0` (because `+` binds tighter than `|`), so a rendered 500 page body, the express-middleware error handler, and the `protocol:set` / `port:reset` / `project:add` CLI error output all reported `0` instead of the cause. They now use a logical `||` and surface the real error stack/message. * A server-side form-validator query rule no longer re-points the framework's process-wide cache onto a second Map: the rule's hand-built controller now reuses the live server instance (published at server start), so concurrent renders keep reading their cached entries and pooled HTTP/2 sessions land in the real server's cache instead of an orphan store on the bundle's config dict * Fixed bundle:openapi and bundle:mcp advertising .exclude()d DTO fields in response schemas — the 200 content schema and the MCP tool outputSchema now emit the response projection (toJsonSchema with dropExcluded), dropping fields the wire can never carry from properties and required[]; request-side requestBody / inputSchema.body keep the declared shape (#B110) * Fixed log messages containing a $ being mangled by the text formatter — the %-token splice used a string replacement, which dollar-expands $-sequences in the message (a $ followed by a backtick was replaced by the rendered log prefix itself, recurring at each occurrence); the formatter now uses a function replacer so messages render verbatim across all levels and all sinks (stdout, mq, file) (#B108) * The in-process render/output cache no longer orphans `fs`-strategy files on TTL expiry: an entry expiring on its timer previously skipped the eviction cleanup, leaving its cached body and `.meta` sidecar on disk and its event registrations stranded. Timer expiry now runs the same cleanup as a manual eviction (#B113). * The validator's forced rule self-injection for boolean-classified checkboxes (and boolean-valued radios) now appends `isRequired` before `isBoolean`, so the engine's false-value rescue applies and an unchecked box whose rule declared neither key no longer fails validation with "Cannot be left empty" ## 0.5.17 - 2026-07-13 ### Fixed * Cleaning up a stale project registration is now reliable. `gina project:rm @ --force` (the `:rm` alias) removes the entry instead of erroring — the alias had failed an internal `--force` guard that only the full `project:remove` form passed. And `gina project:remove` / `project:rm @ --force` no longer crashes with an `ENOENT` error when the stale project's path can no longer be created (a top-level path such as `/app`, or any path under a read-only parent): it skips the now-pointless project-directory re-creation and goes straight to registry-only removal, cleaning both `~/.gina/projects.json` and its state-store mirror plus the project's port assignments — without resurrecting an empty skeleton directory. * Installing gina with the npm 12 `--allow-scripts=gina` remedy (or `npm config set allow-scripts=gina --location=user`) no longer fails with `EALLOWSCRIPTS`. npm exports the allowance to install-script children as `npm_config_allow_scripts`, and npm rejects it in project-scoped installs — which killed the nested framework-dependency install inside post-install, and with it the whole `npm install -g gina`. The nested install now drops the inherited allowance for its duration (the framework dependencies carry no install scripts, so nothing is lost). This unblocks the npm 12 install path, where install scripts are blocked by default and the flag is required. * Corrected a typo in `gina bundle:start`'s scope-resolution expression, which read a misspelled property name. **No behaviour change** — the local variable it assigns is never read, and no bundle manifest carries the per-bundle property (nothing writes it), so the misspelled branch was unreachable: no bundle was ever started with a wrong or undefined scope. A bundle's effective scope continues to resolve, unchanged, from `--scope=`, then `NODE_SCOPE`, then the project's default scope, then the framework default. ## 0.5.16 - 2026-07-12 ### Added * Multipart text fields are now captured into the request body: `request.body` (and `request.post`/`request.put`/`request.patch`) carries the non-file fields of a `multipart/form-data` request — bracket-notation names arrive nested and values verbatim (no url-decode, no boolean/null coercion), capped by the new `upload.maxTextFields` (default 1000) and `upload.maxTextFieldSize` (default 1MB) settings with a 400 on breach. Previously every text field of a multipart request was silently dropped. ### Fixed * `$form.send(FormData)` no longer drops the non-file fields when the payload also carries files — they ride the multipart body as standard text parts and reach the server nested exactly as on the JSON (fileless) path. * A routing rule's `param.title` now reaches `page.view.title` — the routing-param title promotion had been silently inert since its introduction, so the browser-tab title always showed the route name; the stripped route name remains the fallback for title-less rules, and a controller-set title still wins. The `view:add` layout boilerplate now reads `page.view.title` / `page.view.lang` instead of the never-populated `page.title` / `page.lang`, so freshly scaffolded pages get a real tab title and lang attribute. * `settings.i18n.cookieName` is now honoured by the per-request locale negotiation — the cookie name was hardcoded to `gina_culture` and the shipped setting silently ignored; a bundle can now rename the locale-persistence cookie, and an explicit `null` disables cookie-based negotiation as documented. Absent or empty values keep the historical default. * The controller locale-DB fallback is guarded — a bundle without a `region` settings block no longer answers 500 when a request's language is missing from the region data, and the fallback language now resolves deterministically (`region.isoShort` → legacy `region.shortCode` → `en`) instead of silently returning the first region record with an `undefined` in the warn. * The `gina help i18n` text no longer claims `--output` works for `scan --format=json` — the flag applies to `export` only (scan prints to stdout). ## 0.5.15 - 2026-07-11 ### Added * A popin/dialog trigger can now opt out of the hover/focus preload with `data-gina-dialog-preload="false"` (honored on legacy `data-gina-popin-url` triggers too; the value is matched case-insensitively so a templated "False" cannot fail open): the warm-up GET no longer fires on hover or focus for that trigger, and its click loads normally, at click time. Declare it on any trigger whose GET has server-side effects — the preload otherwise assumes GET is safe to fire early, per HTTP semantics. * Client-side components — standards-based Web Components conventions with zero dependencies: `gina view:add` scaffolds now ship a reference custom element (a server-rendered light-DOM partial paired with a behavior-only class — HTML and JS stay separated, repeatable markup clones from a server-rendered `