3408 Commits

Author SHA1 Message Date
Andrés Moya
526ae81396 💄 Extract a function to common module 2026-10-28 12:53:06 +01:00
Eva Marco
6fca46caab 🐛 Skip shape validation when applying transforms
generate-update-shapes validates every instance root of the updated
shapes, including all their children. A variant switch syncs the
copies of the changed main in separate commits, and the layout update
it schedules runs 100 ms later through apply-modifiers. When that
update lands before the sync ends, the validation finds a nested copy
whose component-id still differs from its updated main, and raises.
The layout update stops halfway, and the composable test suite fails
in VariantSwitchPropagates.

Transforms only write transform-attrs (geometry, fills, strokes),
which can't break the references between components. Add a
:skip-validation? option to generate-update-shapes and update-shapes,
and set it in apply-modifiers* and apply-wasm-modifiers.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
2ba607cd5a 📚 Make variant docstrings independent of the callers
The docstrings of generate-add-new-property and
generate-make-shapes-variant, and a comment in
generate-make-shapes-no-variant, described who calls them and how.
Say instead that they are part of a higher level operation, and that
the validation must run once the whole operation is complete.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
911dea8a4f 🔥 Remove debug prints from shape validation
Drop the println in the :validate-shapes change and the prn calls in
the main instance check of the file validation.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
8a3dd86c72 🔧 Validate the variant when renaming or removing a property
generate-update-property-name and generate-remove-property change all
the components of the variant, and validated each main instance. The
container validation checks every main instance too, and also that all
the components keep the same property names, which is what these
functions may break. Validate the container instead.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
743431bd42 🐛 Validate pasted shapes where they are pasted
generate-duplicate-changes looked for the instance roots of the copied
shapes in the page they were copied from, and mapped them to the new
ids. Pasting a main into another main creates a nested copy, but the
copy was validated on its own and reported :nested-copy-not-allowed.
Shapes copied from inside a component and pasted outside were not
validated at all: the unchanged source root was validated instead.

Look for the instance roots from the new shapes, in the objects of the
changes builder, where they already are in their new place.

This fixes three tests in frontend pasting_in_containers_test.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
e702b44121 🔧 Validate variant containers after moving shapes into them
generate-make-shapes-variant does not validate, as its callers may run
it before the shapes are inside the container. Validate in the callers
instead, once the action is complete:

- generate-relocate validates the variant containers the variants come
  from (unless they are deleted for being empty) and the one they go
  into.
- prepare-restore-component validates the container when it restores a
  component into a variant.
- duplicate-variant validates the container after moving the new
  variant into it.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
31248ac115 🔧 Validate the variant after adding a new property
generate-add-new-property changes all the components of the variant, so
it now validates the variant container.

generate-make-shapes-variant calls it while some shapes of the container
may not be variants yet, e.g. when a component is restored into the
container, which gave a false :main-instance-not-a-variant. Reordering
the calls changes the resulting property names, so add a
:skip-validation? option instead. generate-make-shapes-variant uses it
and does not validate on its own: its callers validate the container
once the shapes are inside it.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
16c381ccef 🐛 Create the swapped copy with its swap slot already set
generate-new-shape-for-swap set the swap slot after instantiating the
new copy, through change-touched. The validation made when the copy is
instantiated ran before that and reported :missing-slot.

The slot only depends on the old shape, so compute it before the copy is
created and pass it to generate-instantiate-component with the new
:swap-slot option. The copy is added with its slot, and the validation
sees it complete. The resulting file and its undo are the same as
before.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
b136097e18 🐛 Validate the enclosing root when instantiating a nested copy
generate-instantiate-component validated the new copy on its own. When
the copy is created inside another component it is a nested copy, but
the validator starts with a :not-component context and reported
:nested-copy-not-allowed, a false error that broke 34 common tests on JS.

When the new copy is not an instance root, validate the instance roots
that hold its parent instead. The validator then walks the copy with the
right context, and also checks the parents that may change (e.g.
layouts).

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
05ea874f79 💄 Fix formatting in variant and library files
Fix whitespace reported by check-fmt:clj in files/variant.cljc,
logic/variant_properties.cljc and logic/libraries.cljc.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
36a0ac0a04 ♻️ Use if instead of or and when in builder fallbacks
prepare-restore-component and generate-delete-shapes fall back to other
data when the changes builder has no library data, objects or page. Use
an if on the has-*? check instead of (or (when ...) ...), which is
easier to read.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
7fee18dc1a ♻️ Add skip-validate-changes helper
apply-changes-local and the page index worker drop the :validate-shapes
changes with the same inline filter. Move it to
app.common.files.changes as skip-validate-changes, next to the
:validate-shapes change, so it is easy to find every place that skips
the validation if it changes in the future.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
5ad391eafa 🐛 Skip update-shapes validation when changes have no page
generate-update-shapes only validates changes made on a page, and it
checked that with (and page-id ...) after pcb/get-page-id. The getter
now asserts that a page-id was given, so the check could never be false
and changes on a component container would fail on the assert instead of
skipping the validation.

Add pcb/has-page-id? and check it before calling the getter.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
c16b439af9 🔧 Validate shapes after removing them from a variant
generate-make-shapes-no-variant now validates the main instances it
takes out of the variant. Only those main instances change. The old
container is not validated, as callers such as generate-relocate call
this function before they move the shapes out of it.

Check with thf/validates-shapes-last? that the function validates the
expected page and shapes.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
ae1b541208 🐛 Fix assertion when restoring a deleted component
The changes builder getters now assert that their context was given,
but prepare-restore-component and generate-delete-shapes relied on them
returning nil to fall back to other data. The workspace restores
components with changes that carry no library data, so restoring a
main component or a variant from one of its copies failed.

Add has-library-data?, has-objects? and has-page? to the changes builder,
and check them in those fallbacks before calling the getters.

Add tests that restore a component, and a variant whose container was
deleted, with changes that carry no library data, as the workspace does.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
f2c026dd5d 🔧 Validate shapes after reordering variant properties
generate-reorder-variant-poperties now validates the variant container
when the reorder changes something. All the components of the variant
change, so validating the container checks every main instance and that
all of them keep the same order of properties. A reorder that changes
nothing still returns the same changes, with no validation.

Check with thf/validates-shapes-last? that the function validates the
expected page and container.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
116ca85ea2 🔧 Validate shapes after setting a variant error
generate-set-variant-error now validates the main instance of the
component once its :variant-error is set or cleared. The change only
touches that main instance, so there is no need to validate the whole
variant container.

Check with thf/validates-shapes-last? that the function validates the
expected page and shapes.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00
Eva Marco
d4c01ae0b1 🔧 Validate shapes after updating a variant property value
generate-update-property-value now validates the main instance of the
component once the new value is set. The change only touches that
component and its main instance, so validating the whole variant
container would also check the variants that did not change.

Add thf/validates-shapes-last?, a test helper that checks that the last
redo change is a :validate-shapes change for the expected page and
shapes, and use it to test the function. Register
variant-properties-test in the runner so that it also runs on JS.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:26:04 +01:00
Eva Marco
6c0233d0dd 🐛 Skip shape validation when applying changes locally
apply-changes-local replays the pending redo changes on a local copy of
the data, so that later builder calls can read up-to-date values. It
replayed :validate-shapes too, so the validator ran on that copy, which
has no real file id and no libraries, and reported false errors such as
:component-main-external. This broke Ctrl+K on a component when it
duplicates the main instance and resizes the variant container.

Skip :validate-shapes when applying changes locally.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:26:04 +01:00
Andrés Moya
f041d7cb4e 🔧 Add tests and validations to variant functions 2026-10-28 12:26:04 +01:00
Andrés Moya
ffbd1dfda5 💄 Rename props -> properties 2026-10-28 12:26:04 +01:00
Andrés Moya
0300a39a23 🔧 Validate changes immediately after processing them 2026-10-28 12:26:03 +01:00
Alejandro Alonso
4dc684dd9c Merge remote-tracking branch 'origin/staging' into develop 2026-10-28 11:50:56 +01:00
Pablo Alba
0532f15cee
🐛 Fix tab on variant name doesn't validate formula (#12655)
* 🐛 Fix tab on variant name doesn't validate formula

* ✨ Move helpers to common

* ✨ Mr proposed changes
2026-10-28 11:44:38 +01:00
Alejandro Alonso
a8e8a54a2b Merge remote-tracking branch 'origin/staging' into develop 2026-10-28 10:35:31 +01:00
Pablo Alba
c65a1a3c5e 🐛 Fix swapped nested component loses absolute position after a lib update 2026-10-27 09:46:32 +01:00
Eva Marco
01e0b32850
🐛 Remap library refs inside deleted components on absorb (#12232)
* 🐛 Remap library refs inside deleted components on absorb

Deleted components keep their shapes in :objects, to be restored and
to resolve their remaining copies. When a library was absorbed into a
file (export with libraries merged, unsharing or deleting the
library), absorb-assets only looked for usages in pages and in live
components. The shapes inside deleted components kept pointing to the
library, so they no longer matched their copies in the pages: the file
failed validation with :component-id-mismatch and could not be saved.

Also look for usages in deleted components, so their library
components, colors and typographies are remapped to the local file.

Relates to #10806

AI-assisted-by: claude-opus-5-5

* 🐛 Persist pointer-map fragments when absorbing a library

absorb-library-by-file saved the file without its :features. Without
"fdata/pointer-map" there, encode-file does not persist the changed
fragments, so in files with pointer maps the pages and components
that absorb-assets remapped were never written. After unpublishing or
deleting a library, its consumers failed to load with "fragment not
found".

Pass :features to update-file!. Exporting with shared libraries does
not go through this path, so it was not affected.

AI-assisted-by: claude-opus-5-5
2026-10-26 17:56:31 +01:00
Pablo Alba
f2e5b7e20e
🐛 Fix swapped nested component loses absolute position after a lib update (#12556) 2026-10-26 15:24:18 +01:00
Eva Marco
86e01d56a2
🐛 Update canvas and keep local sets when switching tokens source (#12442)
Setting a library as tokens source did not update shapes that use tokens,
because set-tokens-source never ran the token propagation that every other
token operation runs. Run it after the commit.

Going back to the local tokens, or unlinking the library that was the
source, left no local set active: the file has a single tokens status, and
switching to a library replaced it with the library's one. Keep the status
of the file's own tokens lib aside in a new optional :own-tokens-status
attribute while an external library is the source, and restore it when the
file is its own source again. Exporting without libraries and absorbing a
deleted source library restore it as well.

Closes #12430

AI-assisted-by: claude-opus-5-5
2026-10-26 15:23:05 +01:00
Andrés Moya
6f185c624f
🐛 Fix swap when the original main component is not available (#12333)
* 🐛 Fix swap when the original main component is not available

* 🐛 Change the shape-ref for the id
2026-10-26 12:25:00 +01:00
Pablo Alba
3cd073fc6a
✨ Add auto-repair for integrity error files (#12524)
* ✨ Add auto-repair for integrity error files

* ✨ MR changes
2026-10-26 11:04:06 +01:00
Andrey Antukh
74add05226 Merge remote-tracking branch 'origin/staging' into develop 2026-10-23 13:40:44 +02:00
Andrey Antukh
c04af5ddc5 Merge remote-tracking branch 'origin/main' into staging 2026-10-23 13:11:52 +02:00
Andrey Antukh
76a83387d9
⬆️ Update JVM, Node, pnpm and devenv dependencies (#12600)
Move the backend JDK to its latest maintenance build within Java 26 and
refresh the dependency versions pinned by hand in the development and
production images.

- Backend: Zulu JDK 26.0.2 to 26.0.2.1. The jlink module list keeps
  jdk.management, so the prometheus process metrics stay exported.
- Node.js 24.22.1 in .nvmrc, the development image tarball and the two
  plugins deploy workflows that pin it.
- pnpm 12.15.0 in the four Dockerfiles, the 34 packageManager fields and
  the eleven lockfiles.
- Development tools: clj-kondo 2026.10.20, Babashka 1.13.226, Pixi
  0.83.0, GitHub CLI 2.103.0, AWS CLI 2.38.5 and uv 0.13.5.
- Infra services: RustFS 1.0.3, Mailpit v1.32.0, and Valkey pinned to
  8.1.10 instead of the floating 8.1 tag.

Fix SERENA_UPDATE_VERSION in the compose file. A value there makes the
start script reinstall that Serena release on every start, overriding
the one baked into the image; it was replacing 1.7.0 with 1.6.1.

The hardened base image updates are left to Renovate.

Closes #12599

AI-assisted-by: deepseek-v4.1-flash

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-10-23 12:36:11 +02:00
Florian Schrödl
9aa78a536b
✨ Resolve tokens with the tokenscript-interpreter package (#12348)
Replace the @penpot/tokenscript npm package (the TypeScript
interpreter) with penpot/tokenscript-interpreter, a Clojure and
ClojureScript interpreter that now lives in its own repository and
compiles into the frontend build.

The frontend adapter (app.main.data.tokenscript) resolves tokens with
it behind the existing :tokenscript flag and maps its results to the
values and errors the workspace uses:

- Token values are validated by the interpreter's token types, passed
  by their DTCG names: `1foo` is not a dimension, a number has no
  unit, an opacity is between 0 and 1. Penpot only adds its own
  limits (font weight names, text decoration keywords).
- Division by zero, incompatible units, unknown functions and
  function arguments out of range get messages of their own, and
  token pills show the message of their error.
- Font family names are quoted before resolving, as in the adapter
  this replaces (#11750): `Red Hat` stays one name.

The token form's value input passed the token name to the tokenscript
resolver in place of the typed value, and no name at all for a new
token. It now resolves the typed value under the form's token name, as
the StyleDictionary path does.

StyleDictionary stays the default until the interpreter is stable.

frontend/deps.edn pins the interpreter by tag and commit, v0.1.0.

Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>
2026-10-22 17:32:52 +02:00
Eva Marco
d8862c759b
🐛 Fix shared files 500 on path content without coordinates (#12426)
* 🐛 Fix legacy path migrations on content without coordinates

PRE still returned 500 on get-team-shared-files after #12357, now with
"expected rect instance" in legacy-6. When a path has no points, legacy-6
rebuilds its rect from the content, but content whose points are not
numbers (NaN or infinite coordinates, segments without params, only a
close path) gives no rect either, and rect->points rejects nil.

Fall back to the empty rect in legacy-6. A path with segments missing
coordinates then failed later, in 0003-convert-path-content-v2, since the
binary path data can not store them; drop those segments there.

Relates to #12343

AI-assisted-by: claude-opus-5-5

* 🐛 Skip shared files that fail to load in get-team-shared-files

get-team-shared-files loads and migrates every shared file of the team,
so one file whose migration fails made the whole request return 500 and
the "Add library" modal could not open.

Catch the error per file, log it with the file and team ids, and leave
that file out of the list. The other libraries still show, and the error
report names the broken file.

Closes #12343

AI-assisted-by: claude-opus-5-5
2026-10-21 12:31:03 +02:00
Andrés Moya
3863e92993
🐛 Fix loss of swap slot when copying to clipboard (#12434)
* 🐛 Fix loss of swap slot when copying to clipboard

* 🔧 Add test and make test helpers run same code as real functions

* 🔧 Changes after review
2026-10-21 10:35:40 +02:00
Andrey Antukh
fcf9c27635 Merge remote-tracking branch 'origin/staging' into develop 2026-10-21 10:14:55 +02:00
Belén Albeza
41d95abc5c
🐛 Fix math symbols rendered as boxes in the new render engine (#12471)
Noto Sans Symbols lacks most arrows and math operators. Fall back to
Noto Sans Math for them, and to Noto Sans for letterlike symbols.

Closes #12469

AI-assisted-by: claude-opus-5-5
2026-10-20 14:09:01 +02:00
Andrey Antukh
fffc3ee222 Merge remote-tracking branch 'origin/staging' into develop 2026-10-19 22:18:27 +02:00
Pablo Alba
c7f16d3236
🐛 Keep accented letters in organization avatar initials (#12328)
get-initials used an ASCII-only [a-zA-Z] match, so names like Ábaco
and Órbita skipped the leading accented letter. Match Unicode letters
with \p{L} (CLJS RegExp + u/g flags) so org default avatars match the
expected first letter. Closes #12324.

AI-assisted-by: Cursor <cursoragent@cursor.com>
2026-10-19 15:56:05 +02:00
Eva Marco
4c655e6e9e
🐛 Keep the picture in place in the SVG preview of an image crop (#12387)
With the SVG renderer, resizing a shape with an image fill and the mod
key showed the picture scaled during the drag, as in a normal resize.
The crop only showed after releasing the mouse.

The SVG preview applies the resize matrix to the shape node and ignores
the fill transform change carried by the modifiers. Now the pictures of
the shape fill patterns get a matrix that takes them from the old fill
transform to the new one, which undoes that resize and keeps them in
place. This works for shapes and boards.

Closes #12349

AI-assisted-by: claude-opus-5-5
2026-10-19 13:40:26 +02:00
makesomethingshit
91a5b3a9fb
🐛 Fix token tree path handling for repeated and nested segments (#11591)
* 🐛 Fix token tree collapsing repeated path segments

Tokens whose path contains the same segment name twice in a row
(e.g. test.pointer.pointer) collapsed to a single level in the
token tree.

build-tree-node marked a node as a leaf when the current segment
name matched the first segment of every remaining path. Because
group-by-first-segment rewrites nested paths into the segment
name, a still-nested remaining path was indistinguishable from a
token ending at the current node, so test.pointer.pointer merged
into one leaf.

Mark rewritten segments with an internal ::remaining? marker and
strip it when the rest-path is fully consumed; is-leaf? only
accepts unmarked remaining segments as leaves.

Closes #11588

AI-assisted-by: glm-5.3-flash
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Preserve ancestor tokens in path trees

Keep a node's token and descendants independently when imported paths
contain both an ancestor and a child. Render both in the token tree and
include the ancestor token in existing path-based group operations.

Cover accepted same-set imports, leaf IDs, repeated segments, and internal
marker removal without changing import or collision validation.

AI-assisted-by: GPT-6
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

* ♻️ Rework token tree path building

Carry the segments left to descend as a ::remaining-path vector on each
segment and advance it by subvec, instead of rewriting the segment :name
and re-splitting it later.

With that, build-tree-node finds the leaf token with a predicate over
::remaining-path and takes children from the segments that still have
one, so the pre-grouping children-first sort no longer changes the
result. Drop sort-children-first and its segment-has-children? helper,
and document inside-path?.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Fix and relocate the token tree tests

The ancestor-and-descendant test called a function that does not exist
in app.common.types.tokens-lib, so the namespace failed to load and the
whole common suite errored. Use make-tokens-status-from-lib and the
two-argument get-tokens-in-active-sets instead.

Move the build-tree-root tests to path-names-tree-test so
path-names-test stays a pure split/join unit test.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Refactor the token tree rendering and share path helpers

Extract the token pill into a leaf-node* component and treat a node as a
leaf only when it holds a token and has no children, so a node can carry
both a token and its children.

Make token-separator public and reuse it from the token tree and the
management panel instead of local copies. Both management path filters
inlined the same ancestor-or-descendant rule; extract it as
token-in-path? and cover it with a unit test.

AI-assisted-by: deepseek-v4.1-flash

---------

Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: luisddm <luis.dedios@kaieldos.net>
2026-10-19 09:44:32 +02:00
Andrey Antukh
8c5113b815 Merge remote-tracking branch 'origin/staging' into develop 2026-10-17 14:44:44 +02:00
Andrey Antukh
d1aa36b814 ⬆️ Update pnpm to 12.14.0 and Node to v24.22.0
Bump the pnpm pin to 12.14.0 in the devenv and the production images
and stamp the same version into every package.json, realigning the
drift between the devenv (12.9.1) and the rest (12.6.0).

Bump Node to v24.22.0, the newest patch of the Krypton LTS line, in
.nvmrc, the devenv and image Dockerfiles and the plugins-deploy
workflows.

The pnpm arch checksums are those of the GitHub release tarballs, not
of the npm package. Lockfile changes are metadata only.

AI-assisted-by: deepseek-v4.1-flash
2026-10-17 14:00:43 +02:00
Alonso Torres
02e10a005a
✨ Add token source to plugins (#12183)
* ✨ Add token source to plugins

* ✨ Changes after review

* ✨ Move plugins code to library-edit
2026-10-16 17:58:51 +02:00
Andrey Antukh
03134d35ae
🐛 Fix silent failure when uploading custom fonts (#12315)
* 🐛 Fix silent failure when uploading custom fonts

Custom font upload crashed with re-seq must match
against a string when opentype.js returned nil
subfamily names, leaving the user with no feedback.
Unknown signatures and unparseable files were also
silently dropped.

Make variant parsing nil-safe, harden entry
preparation for missing tables, report every
per-file failure with a bad-font notification, and
show a generic error if the stream itself fails.

Closes #12304

AI-assisted-by: muse-spark-1.3-contributor

* ♻️ Rename font mtype helper and mark helpers private

Avoid confusion with app.worker.import/parse-mtype,
which detects binfile signatures. The font helper now
lives as parse-font-mtype, and the upload helpers only
used internally are private, with tests reaching them
through var references.

AI-assisted-by: muse-spark-1.3-contributor

* 💄 Fix formatting in font upload tests

Realign map keys shifted by the var-quote prefix so
cljfmt check passes again. No behavior change.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Fix infer warnings in fonts metrics code

Add missing ^js type hints to the hhea and os2 metric lookups in
prepare-font so shadow-cljs can infer the interop targets and the
release build reports zero warnings.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-16 12:51:48 +02:00
Andrey Antukh
dfc55a7192 Merge remote-tracking branch 'origin/staging' into develop 2026-10-16 12:48:49 +02:00
Andrés Moya
a5f75c7394
🐛 Fix repair bad variants (#11983)
* 🐛 Ensure normalized names in components and variants

We have a rule of that shape names that include / separator should have
it normalized, with one space before and one after. But this rule is
being enforced only in the UI and the plugins API. There may be other
ways of creating shapes that does not follow it.

For normal shapes this is not really a problem. But for main instances
it may cause that the name of the shape does not match the joined
path+name of the component, and this may generate some problems.

We have decided to add a migration to fix this in all main instances and
variant containers and enforce it in the future with integrity
validations (normal shapes may be left as is).

* 🐛 Fix bad variant-id in the component and the main instance

* 🔧 Fix some issues detected in PR review

* 🐛 Add nil guard
2026-10-16 10:59:57 +02:00