---
title: "v1.7.1"
description: "The release that follows four real 1.7.0 upgrades: `systhema upgrade` keeps edited managed files and always installs the Lexical patch, and a denied role write is refused instead of emptied."
requested_language: cs
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/cs/changelog/v1.7.1
version: changelog
docs_index: https://docs.systhema.app/cs/llms.txt
---
> This page isn't translated yet. Showing English.


## ✨ Highlights

1.7.1 is the release that follows four real 1.7.0 upgrades. Every change here came out of a consumer project moving to 1.7.0, hitting something, and writing it down. The through-line is that the upgrade itself is now a lot safer to run unattended:

- **`systhema upgrade` no longer overwrites a managed file you edited.** It records what it last wrote, refreshes only files that still match that record, and leaves an edited one in place with the new version beside it as `<path>.new`. Two projects lost local edits to their gateway and sitemap files on 1.7.0; that cannot happen again.
- **The bundled Lexical admin patch is installed on every upgrade.** It was previously gated on a codemod that could never fire across a Payload bump, which is every real upgrade, and on some version ranges the stale patch key aborted `pnpm install` after `package.json` had already been rewritten.
- **A denied role write is refused, not silently emptied.** An `admin` without `users.roles.update` could wipe the only administrator's roles with a `PATCH`, locking the site out of its own admin. Both escalation guards now throw a translated error and otherwise leave the value untouched.
- **`systhema migrate --schema` tells you what is wrong with an enum.** Order-only differences are informational, additions emit their own DDL, and a removal prints the values, the rows still using them and the full recreate script instead of a one-line refusal.
- **Generated Payload types and the import map are no longer a function of which secrets happen to be in your shell.** The email schema follows the declared `emails` option; the upgrade also names any import-map key or type export that disappeared during its own regeneration.

Smaller but visible: parallax media never falls short of its wrapper at the defaults, the cookie-consent preferences close button finally has an accessible name in all seven languages, `sync` stops dirtying `.systhema/.meta.json` on every run, the link helper keeps your `onClick` and handles `/#hash`, and the Payload scaffold pins the Payload family to the version the Lexical patch targets.

---

## ⬆️ Upgrade

```bash
pnpm add -g @systhemaui/cli@latest
cd <your-project>
systhema upgrade --dry-run
systhema upgrade --yes --allow-database
```

The upgrade will:

1. Bump `@systhemaui/core`, `react`, `next` and `payload`. No peer floor moves.
2. Run three codemods: `declare-email-schema` (rewrites a pristine generated `src/payload/email.ts` and the `emails:` ternary so the module is always declared and only the key comes from the environment), `rewrite-icon-tag-variants` (`<Icon.svg>` and every other removed `Icon.<tag>` member become `<Icon as="svg">`), and `add-postinstall-cache-refresh` (adds a `postinstall` that restores the package token mirror; it declines when a `postinstall` already exists).
3. Reconcile the bundled `@payloadcms/richtext-lexical` patch in the dependencies phase, after the peer bumps and before the install, sweeping any prior owned entry. The 1.6.0 `install-lexical-schema-dedup-patch` codemod is now a no-op and `systhema doctor --fix` uses the same helper.
4. Run `pnpm install --no-frozen-lockfile` (or the yarn equivalent), because a frozen install is meaningless right after the tool rewrote the manifest. The updated lockfile is part of what you commit.
5. Refresh managed app files through the new ledger (see "Breaking & Behavioral Changes" §1), then run the project's own `sync`, `generate:types` and `generate:importmap`. If that regeneration removed import-map keys or type exports, the run says so and names them; that almost always means an env-gated module was off on this machine.
6. Run the post-upgrade hooks in the documented order: nav renames first, then everything else, then the settings copies. One hook is new: `migrate-capability-strings`, which rewrites stored per-user capability strings after the 1.6.0 and 1.7.0 capability renames (the codemods only ever rewrote source). It writes to the database, so it runs only with `--allow-database`.

**Read the dry run.** The token section now prints only the files a change would touch plus a count of the ones it would not; `--verbose` restores the full per-file listing and `--json` is unchanged.

**On a push-mode PostgreSQL project**, run `systhema migrate --schema --dry-run` between the nav renames and the settings copies, as before. The difference is what it prints when it stops: see "New Features".

---

## ⚠️ Breaking & Behavioral Changes

Nothing here changes a stored data shape. Every entry is either a behaviour the previous version got wrong, or a scaffold default. `systhema upgrade` handles §5, §8 and §10; the rest need no action unless the entry says so.

### 1. Edited managed files are preserved, not overwritten (#207)

`create-app-files --override`, which the upgrade runs, rewrote every managed file unconditionally: no content check, no backup, no per-file line in the output. Systhema now records the sha256 of every managed file it writes in `.systhema/managed-files.json` and decides from it. A file still matching the record is a stale shipped version and is refreshed. A file that no longer matches was edited, so it stays put and the new template lands beside it as `<path>.new`, listed in the output with the instruction to diff and fold. A project from before the ledger existed takes a backup-then-refresh path exactly once, leaving `<path>.bak`, after which the record is exact.

The Payload scaffold tracks `.systhema/`, so commit the ledger. `syncLocaleFiles` uses the same write path, so the managed locale boundary files get the same treatment on plain `sync`.

Two related scaffold changes: the managed sitemap route is now `force-dynamic` instead of `revalidate = 3600`, so a build container that cannot reach the database no longer fails on it (the route sets its own `Cache-Control`, so the edge still caches it); and the `robots.txt` Route Handler is not scaffolded while `src/app/robots.ts` still exists, since Next refuses to build a project carrying both.

### 2. Un-grantable roles and capabilities are refused (#209)

`preventRoleEscalation` and `preventCapabilityEscalation` were written as value filters, and a field `beforeChange` hook's return value replaces the field wholesale, so a request that should have been rejected instead succeeded with whatever survived the filter. When nothing survived, that was an empty array. Both now throw a translated error naming the offending values (`errors:cannotAssignRole`, `errors:cannotAssignCapability`) and otherwise return the value unchanged, so a request that does not carry the field can no longer collapse it. A third guard refuses a change that would empty the acting user's own roles.

An API client that relied on the silent narrowing now gets an error. Trusted server-side writes with no request user, including the seed scripts, take the existing early return and are unaffected.

### 3. Parallax motion at the defaults is narrower (#206)

A translate of `t` percent needs `scale >= 1 + |t| / 50` or the media falls short of its wrapper at the extremes. The shipped defaults violated that: 16 percent of travel against a 1.1666 scale left roughly 50px of the `overflow: hidden` wrapper uncovered. The scale is kept at 1.1666 and the translate range is now clamped to what that scale covers, about 8.3 percent. Raise `scale` to allow a wider range; a configured range the scale already covers is left alone.

The same release fixes the React and Next listeners reading their config at module scope, where the browser store is still empty, so a project's `parallax` settings never reached the engine at all.

### 4. The rich-text block-margin rule matches Systhema classes only (#206)

It matched `[class*="card"]` and `[class*="accordion"]`, so any project class containing the word "card" as a direct child of `.richtext` picked up an `!important` doubled margin. The selector is now built from the token-derived card and accordion class lists. `.figure-w-card`, `.w-card`, `.post-card` and `.highlight-card` are no longer captured; a `figure.figure-w-card` directly inside rich text takes the figure margin instead of the doubled card margin.

### 5. Email schema follows the declared option, not the adapter (#215)

Registration of the General Settings email fields, the email capabilities, the send-test-email endpoint and the form notification email array was gated on an email adapter being present, and the scaffolded module built its adapter from an environment variable. On a machine without production credentials the whole surface vanished from the resolved config, and `payload generate:types` committed a smaller schema. Registration now follows the declared `emails` option. The adapter remains the delivery gate: a boot warning fires when emails are configured without one, and the test-email endpoint returns a clear error instead of disappearing.

A project that declares `emails` but has never had an adapter anywhere gains the email columns on its next push. The `declare-email-schema` codemod rewrites a pristine generated email module to the new shape.

### 6. The cookie-consent seed no longer revalidates during a build (#215)

The first-init seeder wrote the General Settings global from inside a page render without `disableRevalidate`, so a production `next build` ran the `general-settings` `afterChange` hook during static generation, repeatedly and across workers, revalidating every path and firing a Cloudflare whole-zone purge. The write now opts out, a per-process latch stops the repetition, and the revalidation log line names its global and operation.

### 7. Post-upgrade hooks execute in the documented order (#216)

The recovery output has always said nav renames first, settings copies last. Execution ran hooks in registration order, so a settings copy could run before the table it needed existed. Execution now uses the same ordering, and the recovery listing after a failure includes only the failed hook and the ones after it.

### 8. The Payload family is pinned exactly in the scaffold (#213)

`templates/payload` shipped `^3.89.0`. The bundled Lexical patch is keyed to one exact version, so the moment a Payload patch release lands the range resolves past it and `pnpm install` aborts with `ERR_PNPM_UNUSED_PATCH`. New scaffolds pin `payload` and every `@payloadcms/*` dependency to the patch target, and `systhema doctor` now warns when the resolved version differs from it instead of staying silent. An existing project is not re-pinned by the upgrade; do it by hand if you want the same protection.

### 9. The scaffold's `start` is not the platform's start command (#213)

Behind a package manager, a platform's SIGTERM on redeploy produces a non-zero exit and an `ELIFECYCLE` line, so a successful redeploy reports as a crash. Run `node_modules/.bin/next start` directly as the platform start command so Next is the process that receives the signal, and put migrations in a pre-deploy step. The demo container does this now and the deployment docs say so. The template's own `start` script is unchanged.

A rule in the scaffold's admin `custom.scss` that applied a Systhema theme class with `@apply` is removed. Tailwind v4 cannot resolve `@apply` in a file that never references the theme, so it was a build error, not styling.

### 10. `Icon.<tag>` is gone, and now there is a codemod (#210)

1.6.0 removed every `Icon.<tag>` member when it replaced the `Icon` Proxy, filed under drive-by improvements with "no usages". That was only true inside the monorepo; the members were part of the exported type. The `rewrite-icon-tag-variants` codemod turns `<Icon.svg …>…</Icon.svg>` into `<Icon as="svg" …>…</Icon>`, resolves the local import binding so `ButtonIcon.` and friends are untouched, and reports anything it declines to rewrite. The 1.6.0 notes now list the removal as breaking.

### 11. The cookie-consent close button has a default label (#211)

`preferencesModal.closeIconLabel` is part of the translation type with a built-in default in all seven shipped languages, overridable per locale. `consentModal.closeIconLabel` is added to the type but deliberately never defaulted: vanilla-cookieconsent creates the banner's dismiss button only when that string is present, and adding a consent control to every existing banner is not an accessibility fix. The Payload resolver also stops dropping translation keys the Cookie Consent tab has no field for, which previously affected `opensInNewTabLabel` too.

### 12. `sync` is idempotent, and `postinstall` refreshes the token mirror (#214)

`.systhema/.meta.json` gains `artifactsHash` and keeps its `generatedAt` when nothing changed, so a type check or a build no longer dirties the tree. The scaffolds gain `"postinstall": "systhema-core refresh-cache || true"`, which restores the package-local token mirror a fresh install resets; without it a plain `tsc` before the first `sync` resolves theme unions to the shipped defaults. The `artifacts-config` doctor check now actually reports that state, sharing the `sync` fix key with its siblings.

---

## ✨ New Features

### Actionable schema-migration blockers (#216)

`systhema migrate --schema` treated any enum inequality as one opaque blocker. It now classifies the difference. Same values in a different physical order is a warning that prints both orders and generates no DDL. Pure additions emit `ALTER TYPE … ADD VALUE`. A removal still blocks, but the report names each removed value, counts the rows in every column still using it, and prints the complete recreate script (rename the old type, create the new one, move each column with a `USING` cast, restore defaults, drop the old type) for an operator to review. Applying it automatically is deliberately not done.

Constraints are matched by normalized definition as well as by name, so a live foreign key carrying the expected definition under a different name is claimed rather than duplicated. The header nav rename hook warns when it walks a source name PostgreSQL had truncated to its 63-byte limit, which is what left one project with two identical foreign keys.

### The upgrade reports what its own regeneration removed (#208)

`generate:types` and `generate:importmap` resolve the project's Payload config through the ambient environment. The upgrade now snapshots `payload-types.ts` and the admin import map around that step and, when entries disappear, names the removed import-map keys and type exports and says the modules were likely disabled during generation. Removals only; additions stay quiet.

### `systhema-core refresh-cache` (#214)

Refreshes the installed package's token mirror from the project's generated artifacts without running the full sync pipeline. It never throws, prints the `sync` command when `.systhema/artifacts` is absent, and is what the new scaffold `postinstall` runs.

### `Link` handles same-page path hashes and digit-leading anchors (#212)

Both the react and next link helpers now treat an href whose path matches the current pathname, such as `/#contact` from a header call to action, as the same-page anchor it is. The fragment is resolved with `getElementById` rather than `querySelector`, so an id starting with a digit no longer throws and leaves the link dead. Modified clicks, middle clicks and `_blank` targets stay native. The react helper also composes a consumer `onClick` instead of discarding it, which the next twin already did.

---

## 🐛 Fixes & Internal Improvements

- **Zero-valued custom tokens stop flooding the build.** `getResolvedValue` tested the resolved value for truthiness where it meant existence, so a single `letterSpacing: 0` in `customTokens` produced over a hundred "Unable to resolve" warnings per build. The check is now for `null` or `undefined` (#206)
- **The Lexical patch is reconciled on every upgrade.** The 1.6.0 codemod ran before install and gated on the version already in `node_modules`, so it skipped on every upgrade that also bumped Payload, and outside the 1.6.0 window a stale owned patch key survived the pre-flight and aborted the install. Reconciliation now runs after the peer bumps and before the install, gated on the range the upgrade is about to write (#208)
- **`CI=1` no longer breaks the upgrade after `package.json` is rewritten.** The install argv is explicit per package manager (#208)
- **The dry run is readable.** The token section prints what changes plus a count of the skips, with `--verbose` for the old listing (#208)
- **Stored capability strings follow the 1.6.0 and 1.7.0 renames.** The `migrate-capability-strings` hook rewrites `users.capabilities` rows on PostgreSQL, SQLite and Mongo; the codemods only ever rewrote source (#216)
- **The cookie-consent close button has a 24px hit area.** A transparent pseudo-element widens the pointer target without changing the glyph size or any token (#211)
- **`.parallax` no longer declares `object-position: center`.** It is the initial value, so nothing changes, and it was the only declaration in that rule that could ever conflict with a consumer's `object-*` utility (#206)
- **Child-reveal suppression is documented for every component that does it**, not only `.stack`, along with the escape hatches and the `disableAnimation` interaction a custom block author needs (#212)
- **The 1.7.0 notes record the TypeScript 6 incompatibility with `@payloadcms/plugin-mcp`.** The plugin evaluates transpiled schema code through `new Function`, and TypeScript 6 prepends a strict-mode prologue, so every tool registration throws and the MCP endpoint hangs. Not a Systhema bug; Systhema's own loader uses `vm.Script` and is unaffected (#213)

---

## List of all changes

Every commit between `v1.7.0` and `v1.7.1`.

### 🚀 Features

#### cli

- feat(cli): codemod the removed Icon subcomponent variants onto the as prop (#210) (ae99ecb3)

### 🐛 Bug fixes

#### cli

- fix(cli): reconcile the lexical patch on every upgrade and make the run honest about its install (#208) (b40847b1)

#### core,cli

- fix(core,cli): preserve edited managed files and refresh only stale ones (#207) (663c7cfb)
- fix(core,cli): keep sync idempotent and diagnose an unsynced token cache (#214) (935f222b)

#### core,react

- fix(core,react): cover the parallax box, read parallax config at call time, and stop warning on zero tokens (#206) (5f09e5f4)

#### core,react,payload

- fix(core,react,payload): name the cookie-consent preferences close button (#211) (f81b8c3b)

#### payload

- fix(payload): reject un-grantable roles and capabilities instead of silently clearing them (#209) (b449422f)
- fix(payload): stop the cookie-consent seed from revalidating, and declare email schema from config (#215) (cb264162)

#### payload,cli

- fix(payload,cli): make schema migration blockers actionable and fix the upgrade hook order (#216) (e8ff0f61)

#### react,next

- fix(react,next): keep a consumer onClick and resolve anchors the link helper could not reach (#212) (816a60ff)

#### templates/payload,apps/demo

- fix(templates/payload,apps/demo): pin the Payload family and fix the scaffold's deployment shape (#213) (f146a67d)
