Remove per-side stroke widths from both Inspect geometry panels so
Stroke is the only section that lists them. Keep dimensions, position,
corner radii, and transforms in Size and position.
Add rendered-component regressions for boards, rectangles, multiple
selection, uniform strokes, and CSS declarations.
Closes#12026
AI-assisted-by: GPT-6
Signed-off-by: Danial Shirali <danialshirali16@gmail.com>
Signed-off-by: Eva Marco <eva.marco@kaleidos.net>
Co-authored-by: Eva Marco <eva.marco@kaleidos.net>
Stroke width tokens used to be applied to the single `:stroke-width`
token attribute. Applying tokens per side replaced it with the four
`:stroke-width-*` attributes, but no migration moved the existing
references, so files written before 2.19 look unapplied: the token
pill stays off, the Design tab shows a plain number and propagation
finds no update action.
Add migration 0031, which copies the legacy reference to every side,
keeps any side reference already present and drops the old key. It
covers page and component shapes.
Closes#12016
AI-assisted-by: deepseek-v4.1-flash
* ✨ Take changelog highlights from the release highlight label
The 🚀 section was picked by the agent on each run and kept
forever, so a regeneration could add highlights product did not choose
and nobody could drop one. Make the `release highlight` label the only
source, as `breaking change` already is for 💥, and have the report
flag 🚀 entries that do not match the label.
AI-assisted-by: claude-opus-5-5
* 📚 Update changelog for 2.19.0
Regenerate the 2.19.0 section from the milestone with the
update-changelog skill. Add the 79 closed issues that were missing,
drop entries labelled `no changelog` and #9263, which is not in the
milestone, and take 🚀 from the `release highlight` label.
AI-assisted-by: claude-opus-5-5
Wrap every id prop in the comp-processors warn logs
with str so the log shows plain text instead of
the #uuid reader form. Also use :hint instead of
:msg and lowercase text, as the rest of the system
does. No logic changes, logging only.
AI-assisted-by: muse-spark-1.3
* 🐛 Show the size badge on paths and straight lines
The badge was hidden when the shape was smaller than the badge in
either side. Flat paths have a zero width or height, so they never
showed it, and straight two-point paths hid it on purpose.
Paths now only compare their longest side with the badge width. A
straight line compares its length, and the badge runs along the
line, rotated with it and kept upright. Near the bottom of the
viewport the badge moves to the other side of the line.
The badge now reads the size from the shape :selrect, like the
measures inputs, so a rotated path shows the same values in both
places. The three badge branches share a single render block.
AI-assisted-by: claude-opus-5-5
* ♻️ Extract the size badge layout and add tests for it
Move the badge geometry out of selection-size-badge* into the pure
size-badge-layout function, so it can be tested without rendering.
The component now only picks the color and draws the result.
Add unit tests for rects, flat paths and straight lines: when the
badge hides, where it sits, how it rotates and how it moves when it
reaches the bottom of the viewport. Keep the line rotation between
-90 and 90 degrees instead of returning 360 for lines drawn leftward.
The e2e test still expected straight lines to hide the badge. It now
checks that the badge shows and follows the line angle.
AI-assisted-by: claude-opus-5-5
* ✨ Show the length of straight lines in the size badge
A straight two-point path is measured by its length, not by the box
around it, so its size badge now shows the length with two decimals at
most instead of width x height. Other shapes and paths keep width x
height.
The shorter text makes the badge narrower, so it also shows on shorter
lines; the rule is the same, the badge needs the line to be at least as
long as the badge.
Relates to #12064
AI-assisted-by: claude-opus-5-5
* 🐛 Hide the path size badge when the path is lower than it
Paths other than a straight line only hid the size badge when their
longest side was narrower than it, so a wide but low path, or a narrow
but tall one, still showed it. Flat paths no longer need that special
case, since straight lines get their own length badge. Use the same
rule as any other shape: hide the badge when either the width or the
height of the path is smaller than the badge.
AI-assisted-by: claude-opus-5-5
Check a milestone against the Main project board with
scripts/project-anomalies.py (check writes
tmp/<MILESTONE>-ANOMALIES.md): open issues with merged PRs,
milestone mismatches either way, needs triage labels and
unassigned non-community issues. Outsiders resolve in bulk;
only a missing milestone auto-fixes, on confirmation.
Teach scripts/gh.py batched issue lookup (issue subcommand),
milestone on prs output, plus assignees and projects fields,
and retry transient HTTP 504s.
AI-assisted-by: muse-spark-1.3
Move the inline programs out of the update-changelog skill into
scripts/changelog.py (check-merged, cross-ref, report) and shrink
the skill to the workflow alone.
Teach scripts/gh.py to retry transient HTTP 504s, resolve lookups
in batches of 50 (new issue subcommand, milestone on prs output),
and report 💥 entries without the breaking change label.
Add the nine missing 2.17.0 changelog entries found while
trying the new flow.
AI-assisted-by: muse-spark-1.3
In list view the height of each component row depended on its
thumbnail, which renders deferred. When the Assets tab mounted again,
rows were shorter until the thumbnails appeared, so the restored
scrollTop landed several rows further down and scroll anchoring kept
it there. Reserve the thumbnail height in the row grid so rows keep
their size from the first render.
Closes#11971
AI-assisted-by: claude-opus-5-5
Adding a flex or grid layout to a board with children derives the
paddings from the distance between the board and its children. A child
that overflows the board, or floating point noise after "Resize board
to fit content", gave negative paddings. The shape schema rejects them
since paddings must be non-negative, so update-file failed and autosave
stopped. Clamp the derived paddings to 0, as the derived gaps already
are.
Closes#12107
AI-assisted-by: claude-opus-5-5
The tokens list container stays mounted when another token set is
selected; only the restore key changes. With no saved position for the
new set, use-restore-scroll did nothing, so the list kept the scroll
position of the previous set. Scroll to the top when nothing is saved
for the key. Freshly mounted panels are already at the top, so Layers
and Assets do not change.
Closes#11973
AI-assisted-by: claude-opus-5-5
* 🐛 Sanitize smuggled SVG on binfile import
Route imported SVG storage objects through the SVG sanitizer on
all three binfile paths (v3, v1, v2), closing the second bypass
of GHSA-ffhp-m958-qxvr. Integrity checks still run on the raw
bundle bytes first; only the persisted copy is the sanitized one.
Adds a shared sanitize-imported-svg helper with unit tests and a
v3 export-tamper-import test proving smuggled scripts no longer
survive the import.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Normalize SVG content-type on binfile import
Close the residual GHSA-ffhp-m958-qxvr bypass where a crafted
content-type spelling (uppercase, parameters) skipped the import
sanitizer. Detection now uses a shared case-insensitive predicate,
stored values are canonicalized, and the v3 re-read honors the
import size limit.
Extends the tamper tests to obfuscated spellings and adds
exhaustive v1 coverage, including the tempfile branch.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Gate binfile content-type on known storage types
Define the complete set of storage content-types in
app.common.media and enforce it on the binfile import schema, so
unknown types fail closed instead of passing through. Detection
keeps a single normalization at the boundary with an exact
predicate, and the string helpers use cuerdas.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Keep only produced types in storage-object-types
Drop apng, avif, penpot and plain text from the set: no flow
stores them, they only exist in the extension mapping table.
Every member must have a producing code path; unproduced types
stay out until some flow actually stores them.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Reject unknown content-type on v1 binfile import
Enforce the storage content-type allowlist on the v1 import
path, like v3 already does via schema. Bundles declaring types
outside the set, or none at all, fail closed with the same
media-type-not-allowed error as uploads. The sanitize branch is
explicit: SVG bytes are sanitized, anything else passes through.
AI-assisted-by: muse-spark-1.3-contributor
Renaming a token with "Remap tokens" grouped the references by container
and wrote the union of their attributes onto every shape in it. A shape
with a stroke width token on one side got the token on all four sides
when another shape in the same container had it on every side.
Group the references by shape, so each shape is only renamed on the
attributes it already had the token on.
Closes#12075
AI-assisted-by: claude-opus-5-5
Since stroke widths can be set per side, stroke width tokens are stored
in the four per-side attributes, not in :stroke-width. The inspect stroke
panel still read :stroke-width for Border width and had no token lookup
for the per-side widths, so it showed the raw value instead of the token.
Read the token of each side for the per-side widths, and the token shared
by every side for Border width. Files that are not migrated yet keep the
token in :stroke-width, so Border width still falls back to it.
Closes#12029
AI-assisted-by: claude-opus-5-5
* 🐛 Show the top stroke width in the old render
With the old render, the stroke width input showed "Mixed" when the four
sides had different widths or tokens. The old render only draws the top
side, and the input must show it as the global stroke width.
Move the input value and token logic into two pure functions. When per
side strokes are disabled, both return the top side: its width and its
token, or no token when the top has none.
Closes#12063
AI-assisted-by: claude-opus-5-5
* 🐛 Add e2e tests for the stroke width input in both renders
Cover the global stroke width input when the four sides differ. With the
new render it shows Mixed. With the old render it shows the top side:
its width, or its token when the top has one.
The old render cannot create per side data from the UI, and switching
the render reloads the page and drops the test state. The tests patch
the existing stroke token fixture instead, appending the per side fields
to a rectangle and a board.
Relates to #12063
AI-assisted-by: claude-opus-5-5
The nested group wrapper only turned into a wrapping flex row when it
had a direct button child. Since the pills are wrapped in the DS
tooltip (#11900), the button is no longer a direct child, so pills in
nested groups were stacked one per row. Match pills as "any child that
is not a group" instead, so the rule no longer depends on the pill's
own markup.
Also drop the .token-pill rules of this module: CSS modules prefix
them with the token_tree scope, so they never matched the pill button.
Closes#12099
AI-assisted-by: claude-opus-5-5
When the import modal content moved to import-files-stage* (#9958), the
"error list or file rows" if became a when followed by rows that always
render. A non-.penpot file yields an analyze error entry with no name,
so the modal showed an empty error row below the error list. Render the
file rows only when the error list is not shown, as before.
Closes#12116
AI-assisted-by: claude-opus-5-5
* 🎉 Move backend jobs to the unified job table substrate
Replace the generic 'task' table with a unified 'job' table as the
durable substrate for all backend jobs, and migrate every producer
and consumer to it. The legacy 'task' table stops receiving writes
but stays dormant (still cleaned by tasks-gc); upload-session
chunking and the other post-branch upstream changes are preserved.
New 0154 migration creates the 'job' table: dispatch and lifecycle
columns (name, queue, priority, scheduled_at, retries, status,
timestamps, props) plus optional user-facing ledger columns
(profile_id, target, progress, error, result, resource_id,
expires_at). No modified_at trigger: the application updates it on
every write for lease and orphan detection. Partial indexes cover
the dispatcher claim query, orphan scanning, and per-profile
listing.
New app.jobs public API: submit!/invoke!, heartbeats and progress
reporting, a precompiled job-def registry (name, schema, handler,
decoder, validator), an ephemeral request/response mode, and a
management API for external workers. The dispatcher claims
new/retry rows (FOR UPDATE SKIP LOCKED) with a plain JSON redis
payload and marks lease-expired running rows as orphaned; the
runner executes per-queue with the same retry conventions.
New jobs GC (daily): deletes expired rows and retains internal
terminal rows per :jobs-retention, touching referenced storage
objects so storage-gc-touched reclaims them. Job resources live in
a dedicated job-resource bucket with a reclaim branch in
storage-gc-touched.
All production call-sites (sendmail, delete-object, demo-purge,
file-gc, offload, webhooks, quotes, nitrate bulk delete,
snapshots, telemetry) and the ten cron tasks now submit through
app.jobs; cron is a pure scheduler and the legacy worker registry,
task-table inserts, and per-namespace handlers are removed.
Includes migrations, dispatcher, runner, jobs, request/response,
jobs GC, management API, and cron test suites.
AI-assisted-by: glm-5.3-flash
AI-assisted-by: muse-spark-1.3-contributor
AI-assisted-by: kimi-k3
AI-assisted-by: mimo-v2.5
* ✨ Address review comments on jobs substrate
Resolves the four open review threads on the jobs PR. Orphaned jobs
are now marked aborted, a system-side terminal state with no retry:
the dispatcher sweep stores the end event with outcome aborted in a
single bulk insert in the same transaction, records the terminal
outcome next to the orphaned counter, and reports an error log;
orphans keep alerting through the log and the counter, with no
user notification.
The aborted state is terminal everywhere: the status CHECK, the
sweep, the lifecycle writers, the jobs GC retention, the backlog
gauges and the user status mapping, with tests covering each
transition. The claim comment, the 2.20 doc version and the legacy
task table notes are corrected, and objects-gc heartbeats per chunk.
AI-assisted-by: muse-spark-1.3-contributor
Loading a plugin closed every non-background plugin and then emptied the
runtime registry, dropping background plugins (allowBackground, such as
the MCP plugin) that were left running. The registry is what routes a
plugin iframe's postMessage traffic to its plugin and what ɵunloadPlugin
searches, so after the user opened any other plugin the MCP plugin never
received task requests from its UI (MCP tool calls timed out) and could
no longer be unloaded.
Keep background plugins in the registry and drop only the plugins that
were closed.
AI-assisted-by: claude-opus-5.5
Signed-off-by: breken-ai <312387581+breken-ai@users.noreply.github.com>
Co-authored-by: breken-ai <312387581+breken-ai@users.noreply.github.com>
Giant legitimate exports hold hundreds of MB of JSON across
hundreds of thousands of KB-sized entries without tripping any
per-entry cap, so the 200MiB cumulative default rejected them out
of the box. Raise it to 500MiB; the 20MiB per-entry cap still
bounds single-entry inflation. Pin the floor with a regression
test.
AI-assisted-by: muse-spark-1.3-contributor
The S3 file download opens the temp file with CREATE_NEW, so it
fails when the path already exists. tempfile creates the empty
file on reserve, which made every large download depend on the
SDK deleting the placeholder between its own retries.
Add tmp/temp-path, which reserves a unique path and registers
cleanup without creating the file, and use it for S3 downloads.
The old retry on FileAlreadyExists stays as a safety net for a
name clash.
AI-assisted-by: muse-spark-1.3-contributor
Delete the team-scoped file, project and pin relations on leave-team
and delete-team-member, require a live team membership in the file,
project, search and listing permission queries, and clean
pre-existing orphan rows with a migration. Former members now get
not-found on file actions instead of keeping read and rename access
(GHSA-v9r9-h77c-55m2).
AI-assisted-by: muse-spark-1.3-contributor
actions/create-github-app-token v3 deprecates the app-id input in
favour of client-id. Switch to the new TRIAGE_APP_CLIENT_ID secret.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>