Merges develop at f6f9df9a1a. Twelve files conflicted; each keeps
both intents.
files_update.clj: develop's per-stage validation timings wrap the
branch save path. `update-file` keeps the branch's early timer and
passes it to `update-file*`, inside develop's try/catch timing log.
`process-changes-and-validate` times the changes application and
still returns the file, the fixed-up change vector and the media
index. The audit props carry develop's revn and modified-at plus the
branch props. The timing test destructures the new return shape.
v3.clj keeps the branch-file overwrite guard and takes develop's
`configure-import-transaction`. migrations.clj puts develop's 0155
to 0157 before the branch's 0800 to 0805, so the branch's stay the
tail of the vector.
errors.cljs checks the merged and archived branch refusals before
develop's retrying case. left_header.cljs keeps the branch wrapper,
the review badge and the branch bar on develop's markup: the status
dot without icons and the DS library icon on the shared badge.
palette.scss and assets.scss keep the branch clearance terms on
develop's logical properties. The test runners, requires and
deftests are unions.
AI-assisted-by: claude-opus-5-5
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
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>
Take the file row with FOR UPDATE SKIP LOCKED when the offload job
starts and skip the run when the row is locked or gone, so the job
never runs while the file is being edited. Every live hot-storage
row of the file moves to cold storage inside one all-or-nothing
transaction, and the handler answers a summary map with the
offloaded rows, bytes and per-type stats.
Add bounded metrics for the job: rows and bytes counters labeled by
row type on successful runs only, and a timing histogram with the
ok, skip and error outcomes. The job definition now requires
metrics, the recorders live in app.jobs.metrics, and the existing
ephemeral job timing histogram pins its buckets.
Cover the job with a new test namespace: the happy path, the locked
row, missing and deleted files, the rows and bytes summary with its
no-op rerun, the whole-file abort on storage failure, the metric
definitions and emissions, and the submit-time params validation.
An end-to-end suite reads, snapshots, writes, exports, imports and
deletes offloaded files; concurrency tests pin the stale-revn skip,
the blocked-edit retry and the reclaimable orphans of a partial
failure. The file-gc scheduler test pins one job per eligible file,
and the scheduler private helper drops its bang.
Document the lock invariant, the deliberate no-pacing decision and
the metric label sets in the backend storage memory and the
configuration guide.
Related to #12398.
AI-assisted-by: muse-spark-1.3-contributor
* ✨ 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
`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
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
* 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>
* 🐳 Serve the frontend under the path of PENPOT_PUBLIC_URI
Requests below the path are rewritten to the root, so a reverse proxy can
forward them unchanged. The Admin Console keeps receiving the original path.
AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
* 🐳 Strip the public path before proxying to the Admin Console
The Admin Console is served at the root of its own server and applies the
public path at runtime, so it needs a matching Admin Console image.
AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
* 📚 Document subpath deployments in docker-compose.yaml
AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
* 📚 Describe subpath deployments without proxy rewrites
The frontend now serves the subpath itself, so the reverse proxy must
forward it unchanged.
AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
---------
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
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
Both conflicts are tail appends onto regions develop rewrote. In
migrations.clj keep both appended blocks, develop's two entries
first so the branch's four stay the tail of the vector. In
climit.edn take develop's rewritten entries and re-append the
branch's provisional block, moving the map's closing brace to the
last line.
AI-assisted-by: glm-5.3-flash
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
Document stroke per side and its tokens, path editor changes, image
bounds resize, configurable shortcuts, tokens from a shared library
as tokens source, canvas background color tokens, Penpot board
presets, the 3x export scale, the text editing context menu, the
organization and team selector, the hide resolved comments option,
account security and the Check for updates option.
Fix outdated notes about WebGL exports, background blur exports,
library token import and path editor shortcuts. Add the images for
these sections and remove the ones no longer used.
AI-assisted-by: claude-opus-5-5
Add a per-process availability flag (app.health) that /readyz
reports as 503 OFFLINE while offline, so the load balancer
drains the instance and in-flight requests finish untouched.
Operator surface is app.srepl.main/set-offline and
app.srepl.main/set-online; a restart comes back online.
Covered by backend-tests.health-test; HA docs updated.
AI-assisted-by: muse-spark-1.3-contributor
The configuration docs claimed it is not possible to add someone to a
team without them accepting an invitation email, but the backend
auto-adds existing profiles to the team when the email verification
is disabled (create-invitation in app.rpc.commands.teams-invitations).
Replace the incorrect sentence with an accurate exception note, add
the missing disable-email-verification entry to the "Other flags"
reference, document the CLI-created profile auto-add behavior in the
docker getting-started guide, and describe the shortcut in the
developer authentication docs.
Closes#11424
Reported in #11376
AI-assisted-by: glm-5.3-flash
The footer had a hardcoded 2025 copyright and links that no longer match
penpot.app. The year is now set at build time. Sections, links and
social icons follow the current penpot.app footer, and the grid wraps
into two rows to fit the new sections.
Closes#12132
AI-assisted-by: claude-opus-5-5
A plugin with "scope": "global" keeps running across the dashboard
and files and can find and open the user's projects and files. Any
manifest could ask for this without the user being told.
Add the allow:global permission. The plugin registry adds it to
every manifest with global scope, so the install dialog lists it,
and a plugin that becomes global in a later version shows the
permissions update dialog again. The permission is valid in the
shared schema, which the backend checks when saving, and in the
runtime manifest schema. The built-in MCP plugin declares it too.
Document the scope property and the new permission for plugin
developers.
Show the MCP menu next to the layout toggle in the header of the
projects, files and deleted pages, instead of in the sidebar. It
uses the secondary button style there and a visible dot colour
while disconnected.
Clicking the MCP button now toggles the menu instead of only
opening it. Dashboard events report "dashboard:header" as their
origin.
Connection status:
- Replace the dashboard's action-only link with an explicit connected
label and indicator, using the theme's existing accent colour.
- Reflect the actual connection state independently of connection intent.
Shared controls:
- Extract the toolbar's MCP menu and styles into a shared component used
by both the dashboard and workspace.
- Provide connection status, session ID, copy, connect and disconnect
actions in both locations, preserving hover and keyboard behaviour.
- Keep dashboard and toolbar event origins distinct and update the
dashboard connection instructions.
Validation:
- Confirm dashboard disconnect/reconnect, menu opening, keyboard
navigation and Escape dismissal in the running app.
- Pass live compilation, targeted ClojureScript and SCSS lint,
formatting checks and git diff checks.
AI-assisted-by: gpt-6
Signed-off-by: Dominik Jain <dominik.jain@oraios-ai.de>
Global plugin lifecycle:
- Add optional manifest scope "global" for plugins that remain active
across dashboard and workspace navigation within one browser tab.
Existing plugins retain workspace scope by default.
- Preserve global sandboxes when a workspace ends or another plugin
loads, including when that other plugin fails to initialize.
- Avoid duplicate instances of an already-running global plugin.
- Close global plugins on explicit closure or logout, with idempotent
cleanup of listeners, timers and UI resources. Global scope does not
imply automatic startup or persistence across a browser reload.
Plugin API and navigation:
- Keep a permanently live penpot facade whose getters follow the current
workspace, while user, theme and UI facilities remain available on
the dashboard.
- Expose penpotMgmt to global plugins with workspace metadata,
none/loading/ready status and workspacechange subscriptions.
- Add awaitable openFile(fileId, {teamId}) navigation in the current tab,
defaulting to the current team and resolving only once the requested
file and its active page are ready.
- Reject invalid identifiers, failed or superseded navigation and waits
exceeding 30 seconds; release navigation watchers on completion.
- Guard workspace-bound operations while no workspace is ready and
return null from root, currentFile and currentPage in that state.
- Require content:read for the initial management API. Callers must
reacquire file, page and shape references after changing files.
MCP lifecycle and connection controls:
- Make MCP a global plugin and initialize its integrated controller per
authenticated application session instead of per workspace.
- Load the enabled integration with a valid token on the dashboard as
well as in a file, retaining its existing hidden-iframe implementation.
- Keep enabling and connecting separate: an enabled plugin starts idle,
and each tab connects explicitly through dashboard or workspace
controls.
- Preserve the iframe, WebSocket, heartbeat and reconnect machinery
during navigation; disconnect on request and close on disable/logout.
MCP sessions and execution:
- Derive the short MCP session ID solely from currentUser.sessionId,
excluding the file ID. Retain the SHA-256-based, 50-bit Base32 encoding
to produce a stable, copyable 10-character identifier.
- Allow sessions without an open file and update workspace metadata over
the existing connection without replacing its routing identity.
- Refresh context after the connection handshake so initialization does
not leave stale file metadata.
- Expose penpotMgmt to execute_code, preserve execution storage across
navigation and skip layout waits when no workspace is ready.
- Update API types, tool guidance, documentation and the changelog.
Validation:
- Cover authenticated startup, workspace survival, logout cleanup,
readiness, navigation failure, duplicate loads and session routing.
- Verify that short IDs remain stable across files and reconnects and
that disconnect cancels pending asynchronous ID initialization.
- Pass 16 focused frontend tests, 73 runtime tests, 30 MCP plugin tests,
builds, TypeScript checks and frontend/runtime lint.
- Pass all 73 MCP server tests with --test-force-exit; the normal runner
still stays alive after assertions finish.
- Verify a live dashboard-to-file-to-dashboard round trip without a
document or iframe reload, preserving facade identity and storage.
AI-assisted-by: gpt-6
The page described a feature that changes daily behind the `:branching`
product flag, and every change cost two descriptions: the page and the
design notes the review reads. The page now names the featureset it
will cover, branches, pull requests, reviews and merges, and promises
its description at the point the feature is proposed for merging into
develop, written against the code that the proposal ships.
The name moves with it, because the page covers version control for
files and not one of its mechanisms.
AI-assisted-by: mimo-v2.6-pro
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
The backend answers a :conflict refusal (:file-modified, :file-locked)
with HTTP 409 since the previous commit. On the frontend that type had
no ptk/handle-error method, so the version-restore flow would have
fallen to the generic toast instead of the error page the old 500
reached. errors.cljs now maps :conflict to the same exceptional-state
handler.
The technical guide's audit paragraph said rename, archive and delete
report nothing; they report a duration and an outcome. The merge step
list now records the re-validation after the repair and its refusal,
and the update section names the same refusal.
AI-assisted-by: mimo-v2.6-pro, claude-opus-5-5
The guide still opened by calling a branch an isolated copy of a file,
called the merge base a snapshot frozen at creation and the op log
append-only, and gave the creation transaction a reason for deferring
constraints that does not hold. It now says a branch copies no data,
that an update from main repositions the base and squashes or empties
the log, and that every row referencing the new file is inserted after
it.
The same pass corrects five more statements: base_branch_revn holds the
branch file's own revision; a branch kept after a merge keeps its pin;
op log rows survive a logical deletion and go with the cascade when the
file row is hard-deleted; a resolution may be a per-attribute map; and
the listing now runs through `audited`, while a refusal raised as an
error writes no audit event. The RPC table gains get-branching-limits,
the test lists name all five test namespaces, and the UI and event
lists name the limits, merge, and info dialogs.
AI-assisted-by: claude-opus-5-5
The sentence opened with every mutation and then listed three that are the
exception, which a reader has to reconcile. It now opens with the set that
does attach the three keys: the mutations through `audited` and a save routed
to a branch's op log.
The guide claimed the three keys for every branch operation. The three metadata
mutations and the three reads carry the generic RPC event alone, which records
who called what and no duration, so the section now says which is which.
The guide claimed the three audit keys for every branch operation through
`audited`, which was true of creation, compare, merge, update-from-main and
materialise. A save routed to a branch's op log attaches them on the
`update-file` event instead, because that is the props map the ordinary save
already replaces.
The technical guide documented the full-copy branch model this change
replaces. Update the conceptual model, data model, creation steps,
ahead/behind computation, update-from-main (squash), and the limitations
section to describe the derived-state model.
This commit carries the design-branching preview by Juan de la Cruz:
elhombretecla/penpot:branching at 0ac265a3f5, which holds 45 commits
and 3 develop merges and was last synced with develop on 2026-09-16.
The branch is squashed together with a merge of origin/develop at
7c039231f7, so the tree is the merged result.
The merge resolved conflicts in six files:
- backend/src/app/migrations.clj: develop's 0155 and 0156 steps run
first, then the branching steps; the names differ.
- backend/src/app/rpc/commands/viewer.clj: both requires stay.
- common/src/app/common/flags.cljc: both flags stay.
- frontend/src/app/main/ui/dashboard/file_menu.cljs: "Create branch"
becomes a menu-item* with the same id, guard and handler.
- frontend/src/app/main/ui/dashboard/grid.cljs: the portal goes with
develop's menu rewrite; the branches popover stays in the trigger.
- frontend/translations/en.po: both sides' entries stay, with
develop's source references.
Co-authored-by: Juan de la Cruz <delacruzgarciajuan@gmail.com>
AI-assisted-by: mixed models