24218 Commits

Author SHA1 Message Date
Eva Marco
4816293bff 🐛 Report throttled update loops as unhandled exceptions
The throttled update loop report used the handled-exception event, which
only lands in the audit log: error reports and alerts read
unhandled-exception and exception-page. A throttled loop is an
application defect (its cause has :type :internal), and the point of the
report is to find those loops, so it is now an unhandled exception.

AI-assisted-by: claude-opus-5-5
2026-10-27 14:25:13 +01:00
Eva Marco
eeb89aecab ✨ Report update loops throttled by the update scheduler
When the scheduler throttles an update loop, it only logs a console
warning, so a loop that no longer crashes leaves no trace. Send an
error report too, with the events in flight and the last events, at
most once per report window.

Relates to #12580

AI-assisted-by: claude-opus-5-5
2026-10-23 13:16:04 +02:00
Eva Marco
379470bdd2 🐛 Stop thumbnail subscribers re-rendering on every store change
A thumbnail loaded from the server keeps a media id as :uri, and
workspace-thumbnail-by-id resolves it into a new map on each read. The
ref compared values with identical?, so every root frame, every
component in the assets panel and the interactions layer re-rendered
on every store change, mouse moves included. Compare by value instead.

This does not loop by itself, but it adds work to every commit and
brings other update loops closer to React's limit.

Relates to #12580

AI-assisted-by: claude-opus-5-5
2026-10-23 12:47:05 +02:00
Alejandro Alonso
6508449fa0
🐛 Rebuild text layout when image fills finish loading (#12564)
Text image shaders are baked into Skia paragraphs at layout time.
touch_shapes_with_image only scanned shape.fills/strokes, so text
spans were never invalidated after store_image. Detect text content
image fills and force a layout rebuild before touching tiles.

Closes #12563
2026-10-23 09:55:23 +02:00
Eva Marco
ce5276065e
🐛 Skip unknown entries and reserve keys in shortcut settings (#12473)
* 🐛 Skip unknown entries when importing shortcuts

The shortcuts import rejected the whole file when it held a single entry
it did not know, so a profile exported from an older Penpot version, or
edited by hand, could not be imported at all. The spec in #9924 asks to
skip unknown keys, tell the user which ones, and apply the rest.

Importing now keeps the entries that map to a known shortcut with a valid
key combination and skips the rest: unknown contexts, unknown shortcut
keys, and commands that are not a key combination. The import modal
lists the skipped entries. The file is only rejected when it is not a
map or holds nothing that can be applied.

A context whose entries are all skipped is left out: an empty context
clears the user's shortcuts on import, so keeping it would wipe them.

valid-command? reads a command the way mousetrap does, so it accepts
every default shortcut and rejects strings mousetrap could never match.

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

* 📎 Sort requires in shortcuts settings test

cljfmt expects the namespace requires in alphabetical order.

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

* ✨ Reserve cancel, copy and paste shortcuts

Cancel, copy and paste are no longer customizable, in the workspace and
in the path editor, which shares their shortcut keys. Their key
combinations (Escape, Ctrl/Cmd+C, Ctrl/Cmd+V) are now reserved: the
shortcut recorder warns and blocks saving one of them for another
action, and the import skips entries that use them.

Reserved combinations come from the non-customizable workspace
shortcuts, so marking another one as non-customizable reserves its key
too. The path editor's cancel also binds V, the move tool key, so path
shortcuts are left out. Combinations compare the way mousetrap matches
them: aliases and modifier order do not matter.

apply-custom-overrides now ignores overrides of non-customizable
shortcuts. An override saved before this change would otherwise still
apply, with no way to reset it from the settings page.

AI-assisted-by: claude-opus-5-5
2026-10-22 12:24:12 +02:00
Elena Torró
452d751dff
🔧 Fix dashboard thumbnails crashing on deployed environments (#12465)
* 🐛 Pass version tag to worker so it loads the current render wasm

* 🐛 Keep wasm thumbnail queue alive when one render fails

* 🐛 Fail wasm thumbnail request when canvas context cannot init
2026-10-21 16:04:12 +02:00
Elena Torró
05e1981acc
✨ Support running several exporter replicas (#12487)
* ✨ Support running several exporter replicas

* 📚 Document one exporter per machine instead of compose replicas

* 📚 Trim exporter replica docs to the essentials

* ✨ Cancel an exporter's jobs when it shuts down

* 📚 Replace exporter replica docs with a short beta note
2026-10-21 15:30:46 +02:00
María Valderrama
338d009c1e
🐛 Fix sorting for accented teams and organizations (#12505)
* 🐛 Fix sorting for accented teams and organizations

* 📎 Code review
2026-10-21 15:12:41 +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
028547e19b Merge remote-tracking branch 'origin/main' into staging 2026-10-21 10:09:59 +02:00
Andrey Antukh
021492a577 🐛 Make migration 0156 compatible with PostgreSQL 15
`SET STORAGE DEFAULT` needs PostgreSQL 16, but the official docker
images pin `postgres:15`, so migration 0156 failed on those
deployments. The migration now uses `SET STORAGE EXTENDED`, the
storage mode of the type of every column it touches (text, text[],
jsonb and bytea), which leaves the catalog in the same state on any
supported version.

The documented PostgreSQL floor goes back to >=15, the reason for
raising it was that syntax.

Verified on the test database: the 62 columns reset and no table
column keeps the legacy EXTERNAL storage.

The backend memory now records that migrations must stay within
PostgreSQL 15 syntax.

AI-assisted-by: minimax-m3.1-flash-free
2026-10-20 20:36:24 +00:00
Andrey Antukh
3bab9fa0aa 📚 Note the host variable needs three updates
The prod-infra memory now says that the `$host` multi-variable needs
its `query`, its `options` list and its `allValue` regex updated
together. A host present only in `query` is dropped when `All` is
selected, so the panels that filter with `instance=~"$host..."` stop
showing it. Panel 104 is the visible symptom.

AI-assisted-by: minimax-m3.1-flash-free
2026-10-20 20:28:06 +00:00
Andrey Antukh
114928ea5f 🐛 Fix the key reported when a redis DEL times out
`app.redis/del` logged `:key key`, but `key` is not bound in that
`let`: it resolved to `clojure.core/key`, so the line carried
`#object[clojure.core$key ...]` instead of the keys that failed to be
deleted. Log `:keys keys`, which is always the normalized vector, so a
single key and a collection report the same shape.

The tests pin the line: the keys, the hint, the error level, the logger
and the timeout as cause, plus the `nil` the caller gets.

The prod-infra memory now notes that client-side redis timeouts mean the
JVM is paused, not that the server is slow.

AI-assisted-by: deepseek-v4.1-flash
2026-10-20 15:33:37 +00:00
Elena Torró
a1606c42ff
🐛 Export bitmaps over the GPU surface limit through the exporter (#12455) 2026-10-20 15:37:07 +02:00
Belén Albeza
ef68304cb8
🐛 Draw masked group inner shadows in PDF and multi-element export (#12468)
* 🐛 Fix render_to_pdf call in the ToUnicode test

The test passed a scale argument that render_to_pdf does not take, so
the render-wasm test build failed to compile.


* 🐛 Draw masked group inner shadows in PDF and multi-element export

The CPU renderer behind PDF and multi-element raster export drew a
masked group's drop shadow but never its inner shadow, so the masked
content came out flat. Wrap the masked content in a layer that draws
the inner shadows over it, as the GPU single export does.


* 🐛 Export every visible inner shadow of a masked group

The workspace skips shadows too small to see at the current zoom, to
render faster. Export has no such need, so draw every visible inner
shadow instead of filtering by scale.
2026-10-20 15:04:54 +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
Alejandro Alonso
c0450a77c3
🐛 Limit board guide hit testing to the board frame (#12460)
WASM find_guide_at only checked distance on the guide axis, so board-bound
guides stayed interactive outside their board. Respect frame_range in
GuidePool hit testing so interaction matches the drawn solid segment and
the SVG hit area.

Closes #12322
2026-10-20 09:49:05 +02:00
Belén Albeza
60a94c65cc
🐛 Fix missing letters when copying text from exported PDFs (#12441)
Skia maps ligatures and other substituted glyphs to U+0000 in the PDF
ToUnicode maps. Rebuild those maps from the laid-out paragraphs and
append them as an incremental update.

Closes #12393
2026-10-20 09:32:53 +02:00
Andrey Antukh
4635eaa467
🐛 Show request access modal on viewer and dashboard urls (#12456)
The exception page compared the screen query param against literals
that never occur ("view", "dashboard"), so viewer and dashboard urls
of inaccessible files and teams fell through to the plain not-found
page instead of the request access modal. Classify the param with a
tested helper that recognizes "viewer" and every "dashboard-*" route.

Closes #11224

AI-assisted-by: muse-spark-1.3-contributor
2026-10-19 22:33:10 +02:00
Andrey Antukh
e94e148ac6 Merge remote-tracking branch 'origin/staging' into staging 2026-10-19 20:10:29 +02:00
Andrey Antukh
fdd66978fd Merge branch 'main' into staging 2026-10-19 20:10:20 +02:00
Elena Torró
0801674af2
🐛 Ignore export scale in render-wasm PDF export (#12447) 2026-10-19 18:21:02 +02:00
Andrey Antukh
534d46c7b3 📚 Update changelog
Add the missing 2.18.4 entries for the closed storage GC,
version quota and file_change revision issues, with their
fix PRs referenced inline.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-19 15:59:16 +00:00
Andrey Antukh
2d335e9e83 🔧 Reset explicit EXTERNAL storage to PG defaults
Drop the legacy TOAST STORAGE EXTERNAL overrides set when PostgreSQL
ran on ZFS with transparent compression. Migration 0156 restores the
type default (SET STORAGE DEFAULT) on 62 columns across 21 tables;
existing TOAST data keeps its strategy until rewritten.

Raise the documented PostgreSQL floor to >=16, required by the new
SET STORAGE DEFAULT syntax.

AI-assisted-by: muse-spark-1.3-contributor
2.18.4-RC4
2026-10-19 15:37:13 +00:00
Alejandro Alonso
29cd50621d
🐛 Exclude inner shadows from WebGL export bounds (#12427)
Inner shadows were joined into extrect via the drop-shadow filter, so a
Y offset grew the PNG/SVG export surface. Only drop
shadows expand bounds, matching classic export. Closes #12386.
2026-10-19 17:23:26 +02:00
Andrey Antukh
61a66f31ab 🚑 Disable media storage quota check on upload
Comment out the media-storage-bytes-per-team check in
upload-file-media-object. The SUM over storage_object takes
3-12s per upload on prod (3.5TB DB, ~34 slow runs/hour)
and blocks uploads. Keep the SQL and quote definition
for re-enable with a cheaper implementation.

Update the upload quota test to document the disabled
state until the new implementation lands.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-19 15:22:10 +00:00
Belén Albeza
fd03ce2a63
🐛 Show text typed on a new line during the v3 layout preview (#12384)
A modified shape is a cached copy, and edits through get_mut did not
reset it. After Enter the preview stops sending modifiers while the size
holds, so the canvas kept painting the old text.

AI-assisted-by: claude-opus-5-5
2026-10-19 17:17:35 +02:00
Elena Torró
7f53483a32
🐛 Keep full quality rendering during the v3 text layout preview (#12360) 2026-10-19 17:17:34 +02:00
Andrey Antukh
5abf66dbf0 🐛 Set a finite idle timeout on binfile transactions
Binfile v1/v3 import and the v3 export get-file step hold a single
transaction while the ZIP is decoded and the file data is realized.
Disabling idle_in_transaction_session_timeout left a stuck connection
holding locks (and a pool slot) forever.

Raise it to 30 minutes instead. The longest import/export observed in
production is under 8 minutes, so the limit still bounds a stuck
session. statement_timeout stays at 300 s.

Add set-transaction-idle-timeout and configure-import-transaction in
app.binfile.common, use them from both paths, and cover the helper
with a test.

AI-assisted-by: deepseek-v4.1-flash
2026-10-19 16:31:15 +02:00
Andrey Antukh
38a6961ff1
✨ Make keyboard shortcuts layout-independent (#12326)
* Revert "🐛 Fix shortcuts with modifiers on letters and digits (#12292)"

This reverts commit 21ecfb67b70ac2d6f4defb05a543f0fc18d692a2.

* 🐛 Migrate mousetrap to key with sane modifiers

Finish the half-done mousetrap migration from code/keyCode to
key: canonical lowercase tokens on both bind and event sides so
modifiers match again, US-only shift remap and keypress legacy
removed, and AltGr phantom ctrl filtered on Windows.

Record punctuation positionally through getLayoutMap (American
fallback) so Shift+Period stores shift+. on every layout, while
letters and digits stay glyph-based for AZERTY. Drop dead 219
aliases and revert the stray CamelCase escape command.

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

* ♻️ Rewrite mousetrap as ESM class without behavior changes

Turn the 2012 constructor-plus-prototype shape into a class
with underscore-private methods, const tables and arrow
listeners. Drop dead _belongsTo, fold one-line prevent/stop
helpers into _fireCallback, and let handleKey absorb the old
_handleKey indirection. Public API and matching logic stay
identical; verified by existing tests plus a runtime smoke.

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

* 🎉 Add node:test suite for mousetrap shortcuts

Cover canonical key normalization, modifiers with AltGr,
bind arrays plus aliases keyup reset overwrite, sequences
with fake timers, and the full stopCallback matrix, all
through isolated instances and synthetic events. Hook the
zero-dependency suite into frontend/scripts/test for CI.

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

* 🐛 Record shortcuts by glyph instead of position

Drop the getLayoutMap positional layer and the
American fallback: punctuation now records event.key
lowercased, the same token Mousetrap dispatches, so
Shift combos match with and without a layout map.
Keeps the Numpad numN branch both sides share.
Updates the pinned positional tests and adds the
Spanish Shift-plus regression.

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

* 🐛 Resolve shifted digits to the digit itself

Shift+DigitN now resolves to the digit on every layout, so
shift+0 fires on QWERTY ()) as well as AZERTY (0), like the
old keyCode behavior. Applies in the recorder and Mousetrap;
unshifted digits and numpad are untouched. Also stages the
layout-map suite left untracked.

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

* 🐛 Cover punctuation defaults with layout twins

Some defaults only fired where their glyph is unshifted (+ dead
on US layouts, shift++ dead outside ES/DE, \ dead where it
needs AltGr). Bind the twin token per layout family and freeze
the display with :show-command. No tooltip or translation
changes.

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

* ✨ Warn when the keyboard layout API is missing

Shows a warning on the shortcuts settings page in browsers
without navigator.keyboard.getLayoutMap, where Shift with
punctuation may not fire. Uses a synchronous probe, English
and Spanish strings, and documents the layout limitations in
the user guide.

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

* 🐛 Fix hide-ui twins and qualify layout docs

The ctrl+alt+\ twin could never fire (the AltGr filter drops
Ctrl), so it goes away; alt+º covers Chrome on Spanish
layouts where AltGr+º resolves to º. The user guide now
states which claims need the layout map and which hold
everywhere.

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

---------

Signed-off-by: Eva Marco <eva.marco@kaleidos.net>
Co-authored-by: Eva Marco <eva.marco@kaleidos.net>
2026-10-19 16:08:47 +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
Alejandro Alonso
655300e4d1
🐛 Include board drop shadow in multi-element raster export (#12440)
Headless raster/PDF used the board selrect so overflowing children stayed
cropped, but that also clipped the board's own drop shadow. Use
export_root_bounds: own strokes/shadows/blur without joining children.

Closes #12388
2026-10-19 15:29:40 +02:00
Belén Albeza
c99980327f
🐛 Resize fit-content flex boards while typing in v3 text editor (#12330)
* 🐛 Resize fit-content flex boards while typing in v3 text editor

Typing in an auto-width text did not resize its fit-content layout
parent until the editor closed. The v3 editor now previews the live
text size in the layout on each keystroke, also when the text sits
in a group.

* 🐛 Preview text layout when typing with a pending caret style

Both v3 editor commit paths share v3-update-editor-content.

* 🐛 Skip the layout preview for absolute texts

Absolute layout items do not affect their board.

* ⚡ Preview text layout only when the text size changes

Each preview propagates modifiers, so skip unchanged sizes.

* 🐛 Leave interactive render mode when the v3 editor closes

Finalize only ends it when it applies geometry.
2026-10-19 15:06:56 +02:00
Andrey Antukh
688c0fc398
🐛 Keep file_change revisions on their own deletion schedule (#12419)
* 🐛 Keep file_change revisions on their own deletion schedule

Restoring a file from the trash cleared the deletion mark on every
file_change row of that file, revisions included. A revision is born
already scheduled for deletion one hour later, and objects-gc only
looks at rows that carry a date, so unmarking one left it in the
table for ever: production had 358,517 of them across 3,614 files.
The file deletion cascade had the mirror problem: it rewrote the date
of rows that already had one, pushing a revision out to the trash
lifetime.

Restrict the restore to durable history and date only the rows that
have no date yet, so a revision keeps the schedule it was born with.

A pinning test covers the snapshot update path, which cannot reach a
revision because it resolves its row through a read filter. The
backend memories now carry the model of the three file_data classes
and this invariant, in place of the two superseded stubs.

Closes #12418

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

* 📚 Add autovacuum and Postgres exporter notes to Grafana memory

Record what this Grafana instance can and cannot show about autovacuum
so the next session does not have to re-derive it:

- The Postgres exporter scrapes the primary as well as the
  read-only replica, so the primary's pg_stat_activity and
  pg_settings_* are in Prometheus even though the SQL
  datasource cannot reach them.
- It publishes no pg_stat_progress_* collector, and
  pg_stat_activity_count has no backend_type label: autovacuum
  workers are the usename/application_name-less bucket with
  state=active. The launcher shows up as the AutoVacuumMain
  wait event, and it is absent on the replica.
- A metric the exporter does not publish needs a custom query
  block in its YAML queries file and a restart of the exporter.
- The Postgres server ships its own log to Loki (the job is
  postgresql, primary only), whose streams carry only filename,
  instance, job and level. The completed vacuum lines live
  there: a per-table history, not a view of what runs now.
- The PostgreSQL section now ends with the vacuum panels,
  with the Vacuum TXID age table last; an in-flight per-table
  view would need a pg_stat_progress_vacuum collector in the
  exporter itself.

AI-assisted-by: deepseek-v4.1-flash
2026-10-19 14:00:13 +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
Andrey Antukh
84d92cfd5a
✨ Add tail buckets and update-file validation timings (#12409)
* ✨ Extend backend histogram buckets beyond 7.5s

Give every backend timing histogram a tail past the shared 7500ms cap
so slow RPC calls, CLIMIT waits and background tasks report real
quantiles instead of clipping at the top bucket.

Request and wait metrics grow to 60s, background tasks to 300s and
Redis eval to 10s; the sub-7.5s buckets stay untouched to keep the
history comparable.

Closes #12408

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

* 🐛 Fix flaky assertion in climit republish test

Read the last published waiting value instead of the exact
publication list. The sampler thread started with the component
runs its first tick asynchronously, and when it lands inside the
recording window it publishes the same count twice, which made
the full-suite CI run fail while focused runs stayed green.

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

* ✨ Add file_data blob to JSON decode helper

Allow decoding file_data blobs (app.util.blob) to JSON
for investigations with jq, offline from a blob file or
from nREPL with the backend running.

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

* ✨ Add validation step timings to update-file logs and metrics

Measure the six phases inside process-changes-and-validate
(resolve-libs, process-changes, soft and hard affected and
schema validations) with tpoint and try/finally, so timings
are recorded even when a hard validation throws.

Publish each phase to a new histogram
penpot_rpc_update_file_validation_timing with a fixed step
label using the request buckets (up to 60000 ms), and extend
the update-file debug log with the same *-ms fields plus
file, project and team ids. The deferred log only runs on
success, so failures get an inline error log with partial
timings.

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

* 💄 Translate decode-file-data comments to English

Translate the Spanish docstrings, section headers and test
messages of the file_data decode helper to English, keeping
code untouched. The repo convention is English in code and
comments; the helper was first written in Spanish.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-19 13:01:35 +02:00
Elena Torró
c401151541
🐛 Fix mask group drop shadow in PDF export (#12362) 2026-10-19 12:07:51 +02:00
Andrey Antukh
5262aab3ff
🐛 Count saved versions in the version quota (#12405)
* 🐛 Count saved versions in the version quota

The snapshots-per-file and snapshots-per-team quotes counted the
saved versions by file_change.data, a column that only the legacy-db
file data backend writes: versions keep their payload in file_data,
so the count returned 0 and a configured quota never applied.

Count by label instead, which tells a version apart with every file
data backend, and add a partial index on file_change with the same
predicate so both counts are answered by an index only scan instead
of walking every row of every file of the team (10.3 seconds in
production for a team with 829 versions).

Closes #12404

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

* ♻️ Drop the ! suffix from the snapshot quota test helpers

The check helpers assert the outcome of a command instead of mutating
anything, so they do not take the ! that this codebase reserves for
functions whose contract changes state or runs a command (the
th/command! call inside them already carries it).

AI-assisted-by: deepseek-v4.1-flash
2026-10-19 11:15:51 +02:00
Eva Marco
f4f581ab74
🐛 Fix export scale dropdown clipped in Inspect (#12351)
* 🐛 Fix export scale dropdown clipped in Inspect

The export selects in the Inspect tab used a dropdown class that always
opened them upwards, overriding the select's own up/down choice. On
elements with few properties the export section sits near the top of
the panel, so the scale list (now eight options) went past the top of
the scroll area and hid 0.5x. Keep only the dropdown width and let the
select pick the direction.

Closes #12346

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

* ♻️ Move Inspect exports styles to DS tokens

Replace the deprecated spacing and size variables, the flex mixins, the
color aliases and the typography mixins with DS tokens, and use logical
properties. The typographies switch to the closest DS ones: body-small
for the suffix input (line-height 1.3 instead of 1.4) and headline-small
for the export button (12px instead of 11px). The @extend placeholders
still come from refactor/ and are left for a later migration.

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

* ♻️ Use DS buttons in Inspect exports

Replace the add, remove and export buttons in the Inspect exports section
with the DS icon-button and button, as the workspace export section does.
Delete their now unused styles and the sizes import; the export button
class only stretches the button and centers its label.

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

* 🐛 Fix add export button lookup in the Inspect exports e2e test

The add export button in Inspect is a DS icon button now and lost its
.add-export class, so the test timed out waiting for it. Find it by its
role and accessible name instead.

AI-assisted-by: claude-opus-5-5
2026-10-19 10:50:32 +02:00
Eva Marco
a5678657f5
🐛 Fix padding on sets section (#12422) 2026-10-19 10:48:39 +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
49f746ddd8 📚 Never commit the plan file with the implementation
The implement-plan flow asked to commit the closed plan together with
the code, so the resulting commit looked like it was missing a piece.

State the rule in the three places that describe it: the implement-plan
skill, the commit workflow memory and the agents README. A plan is
session bookkeeping, not part of the change, and its absence from the
commit is correct rather than a missing piece.

AI-assisted-by: deepseek-v4.1-flash
2026-10-18 20:40:23 +02:00
Andrey Antukh
16ea1ab3cd 🔧 Ignore generated artifacts and local root files
Local scratch files and autogenerated build output were
showing up in git status:

- /*.html and /*.json cover local reports and token
  dumps at the repo root; !/package.json keeps the
  tracked root manifest visible to git.
- /media-processor/target/ and /scripts/__pycache__/
  cover build and Python cache output.
- render-wasm artifacts copied into the exporter go in
  their own commented block.

Record the exporter and render-wasm generated paths in
the module memories so nobody edits or commits them by
hand.

AI-assisted-by: deepseek-v4.1-flash
2026-10-18 18:01:22 +00:00
Andrey Antukh
70b317a976
🐛 Harden upload path handling and validation error bodies (#12067)
* 🚑 Reject forged upload paths outside multipart

Transit bodies can smuggle upload maps whose path points at any
server file; the schema only checks the shape and handlers open
the path directly.

app.rpc now injects a server-side multipart flag that bodies cannot
forge, and methods declare their upload keys with the new
multipart-only-params metadata, enforced centrally after schema
validation with a params-validation error. Covers all nine upload
methods, including chunked and management flows.

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

* 🐛 Sanitize raw explain in generic validation error bodies

The generic :validation branch returned ex-data untouched, so errors
carrying the raw malli explain (live schema objects, e.g. storage
invalid-storage-metadata) broke JSON encoding mid-stream and clients
got truncated bodies. Apply the same clean treatment the known codes
already use: drop the raw explain and attach its humanized text.

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

* 🐛 Add e2e path traversal tests for binfile and media upload

Add two e2e suites that hand-write transit bodies with a forged
["~#path", ...] upload path (no transit library needed) and expect
400 validation/params-validation. They cover import-binfile :file,
upload-file-media-object :content and upload-chunk :content, plus a
plain-JSON string-path control case. Before the multipart-path fix
they failed (500 io-exception, invalid-image, 200 stored chunk);
after it they pass.

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

* 🐛 Use multipart helper in binfile SVG test setup

The SVG import setup uploaded through plain command!, so the new
multipart-only guard rejected it with params-validation. Switch
the setup to multipart-command! like the other upload tests.

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

* 🐛 Use multipart helper in remaining upload tests

The multipart-only upload guard rejects plain RPC calls carrying
upload maps. Five tests with real files still used command! and
failed with params-validation. Switch them to multipart-command!
while keeping the forged-content tests on plain command!.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-17 16:01:04 +02:00
Andrey Antukh
15606a3f85 🐛 Answer 400 on a request body the parser cannot read
A body declared as JSON that clojure.data.json cannot read ended as a 500
:unexpected with the whole stack trace in the log: wrap-parse-request
classified the failure by exception class, and the library signals its
syntax errors with a bare java.lang.Exception.

Classify it where it happens instead: read-body-params reads the body and
raises :validation :malformed-json (400) for anything it cannot parse,
keeps RequestTooBigException at 413 and re-raises a structured error from
deeper untouched. The body is read outside the handler try, so the error
goes out as a throw like the other specific-exception branches.

The failure is now one log line (path, peer address, content-type, parser
message) without the stack trace, and it is counted: the middleware takes
the metrics instance as a middleware argument, so the backend exports
penpot_http_malformed_requests_total and nexus declares its own
nexus_http_malformed_requests_total.

Tests: parse-request-malformed-json-body reproduces the production body
and asserts the 400 and the counter; the existing parse-request tests now
pass a metrics stub.

AI-assisted-by: deepseek-v4.1-flash
2026-10-17 15:54:52 +02:00
Andrey Antukh
07e085c2cf :revind: Backport several updates for .github/workflows from develop 2026-10-17 14:55:22 +02:00
Andrey Antukh
9336962a77 ⏪ Backport docker changes from develop 2026-10-17 14:54:23 +02:00
Andrey Antukh
231adf4edc ✨ Add a skill to rewrite a squashed commit message
The new skill turns HEAD's stacked subjects, bodies and repeated trailers
(a typical result of squashing) into one message that follows
mem:workflow/creating-commits: a single imperative subject, a body
covering the whole branch diff, then the trailers.

It collects every distinct AI-assisted-by value before rewriting, so an
attribution is never dropped, and it requires ./scripts/check-commit to
exit 0 after the amend. It never pushes: rewriting history over commits
the user already pushed is their call, not the agent's.

AI-assisted-by: deepseek-v4.1-flash
2026-10-17 14:00:43 +02:00
Andrey Antukh
016c3976d0 🔧 Install opencode on demand instead of baking it in
The opencode build stage was already commented out, so the image carried
a dead block and a commented COPY. Drop both: opencode now comes from
scripts/setup-opencode, which installs @opencode/cli as a global pnpm
package in the penpot user's $PNPM_HOME. That dir sits in the user-data
volume, so the install survives a container re-creation and needs no
root.

pnpm 12 keeps the global binaries in $PNPM_HOME/bin, so put that dir on
PATH in the bashrc (and make the guard check the same path) instead of
$PNPM_HOME, which left the commands unreachable.

The entrypoint now also chowns ~/.local, ~/.cache and the dotfiles it
copies from /root. EXTERNAL_UID remaps penpot to the host UID on every
start, so the ownership the image set at build time stops matching; that
broke global pnpm installs and pnpm's own packageManager download.

AI-assisted-by: deepseek-v4.1-flash
2026-10-17 14:00:43 +02:00