mirror of
https://github.com/penpot/penpot.git
synced 2026-11-05 09:30:38 +00:00
320 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bc8d369d69
|
📚 Say why the example climit config carries queues
The comment on the branch limits said the header requires a queue on every entry. Nothing requires one at runtime: climit.clj::wrap skips a declared limit with no configuration and logs a warning. Only the test example-config-bounds-every-wait checks this file for a queue, and the numbers are example defaults, not measurements. AI-assisted-by: claude-opus-5-5 |
||
|
|
0cc2e97338
|
Merge develop into iop-vc-preview
Bring iop-vc-preview up to develop 43f3bda8b5, which carries the final squash of penpot#12420 (93d36d04d8). iop-vc-preview carried an earlier copy of penpot#12420 (42a2e33ffe and a6c20b3e95). Every file the early or the final copy touches was merged three-way with a6c20b3e95 as the base, so develop's side wins for every penpot#12420 hunk and iop-vc-preview keeps only its own deltas. Changes from other develop PRs (#12524, #12192, #12731, #12686, #12442) are kept. Adaptations to develop: - files_update.clj: apply-changes returns the triple [file op-changes media-index] on the #12524 auto-repair path too, and a repaired branch save logs the repair changes ahead of the client's ones. - files.clj: absorb-library-by-file takes develop's update-file! map. - migrations.cljc: migrate keeps its target arity and develop's ordered :migrations set. - climit.edn: the branch global limits declare a queue, as #12686's example-config-bounds-every-wait requires. - Tests: the repair tests destructure the triple, the branch import test uses the #12192 export cfg keys, and the ref-paths test classifies :set-own-tokens-status. - Dashboard comments and placeholder take develop's design-system icons around iop-vc-preview's review list; en.po and es.po keep the union of both sides. AI-assisted-by: claude-opus-5-5 Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> |
||
|
|
65a76a21cd | Merge remote-tracking branch 'origin/staging' into develop | ||
|
|
3cc91e8ba2
|
♻️ Take the command limits before the root ones in the RPC limiter (#12686)
* ♻️ Take the command limits before the root ones in the RPC limiter The RPC concurrency limiter took the root limiters (the ones every request passes through) before the ones a command declares, so a request waiting for the limits of its own command held the root permits of its profile while it waited: a profile with a pile-up of heavy requests saw its other requests queue behind them and be rejected with 429. That order was forced by two commands that entered the limiter from inside their handler: a command that declares no limits of its own can only take them deeper than the root ones, so two paths of one profile could take the same limiters in opposite order and park for good. The commands declare their limits in their metadata now and their handlers call the work directly, so no command enters the limiter from inside its handler and every path takes the limiters in the same order. The root limiters are appended to the sequence first, so the limits a command declares end up outermost and are taken first: a request waiting for them does not hold the root permits of its profile. The photo of a team keeps its limits, because it declares them now that the helper does not. A call from inside a handler can only reenter the limits that thread already holds, and it is refused when it asks for a new one, because that would take it after the root ones and invert the order against the other paths. The key of a limit is resolved the same way in every path, so both paths point at the same limiter and the reentrancy check sees it. Every entry of the shipped climit.edn declares a queue, so the number of requests waiting for the same limit is bounded instead of piling up until the timeout. The production file lives outside the repository and has to be mirrored to pick them up. The tests cover the acquisition order of a command with and without limits of its own, the order of the limits as a whole (a ring of any size fails, not only a pair in opposite order), the nested call that asks for a limit the thread does not hold, and the queues and timeouts of the shipped configuration. The import of a binfile declares its request as multipart in its test, the way the commands that take an upload map do. AI-assisted-by: minimax-m3.1-flash AI-assisted-by: deepseek-v4.1-flash * 🔧 Keep the commands' limits and the shipped config in sync `profile-plugin-ops` guarded only an UPDATE on the caller's own profile row, which the row lock already serializes, so its global cap could only reject a burst of different profiles toggling plugins. Both commands declare the by-profile limit alone now, and the example config drops the entry. A declared id without an entry does not limit anything: `wrap` logs a warning and runs the handler with no limiter at all, which is how a command ends up unbounded in production. A test fails now when a command declares an id the config does not list, and the header of the example says the production file mirrors the shape and lists every declared id, not the values. AI-assisted-by: deepseek-v4.1-flash * 📚 Record that background work is not waited for with a sleep A long command moved to the background reports its own completion, so polling it with a sleep inside a foreground command only blocks the session and the notification arrives anyway. The rule says to keep working on something else, or to end the response and wait for it. AI-assisted-by: minimax-m3.1-flash |
||
|
|
74e33d05b6
|
Merge branch 'juan-vc-ui-tweaks-01' into iop-vc-preview
Takes penpot#12596: branch access by team role alone (no file role on a branch, the owner role on materialise, branches move with their source file, migrations 0807 and 0808), the merge record with its comment (migration 0806) and the reviews dialog, the new git icons, and the workspace tidy-up. Where Juan's later redesign of the compare, conflict and merge dialogs (c68daed816, e4e237346e) already changed the same code, the redesign wins: the old dialogs stay in branch_review/, the redrawn compare icon stays, and the merge keeps its "Archive this branch" option. So merge-file-branch accepts both `comment` and `keep-branch`, a kept merged branch is listed and counted again, and the keep-branch tests the PR removed are restored. The merge dialog asks for the comment beside the archive option, and the conflicts dialog sends it on. AI-assisted-by: claude-opus-5-5 |
||
|
|
1342b27f6f
|
Merge remote-tracking branch 'origin/develop' into iop-vc-preview
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> |
||
|
|
74add05226 | Merge remote-tracking branch 'origin/staging' into develop | ||
|
|
c1dec5d4f0 | ✨ Add merge reviews and tidy up the branching workspace | ||
|
|
d669eec0c0
|
✨ Load the branch list only when the switcher opens (#12571)
* ✨ Add a branch count query for the workspace header The header shows its branch row only when the file has branches, and it learnt that from get-file-branches, which compares every diverged branch against main. get-file-branch-count answers the same question with one indexed count: archived and merged branches included, counted from main when the file is itself a branch, with no comparison and no climit, like the branches_count subquery of the dashboard listings. The listing keeps its climit, and :get-file-branches/global now sets the 60 s timeout that the header of climit.edn promises, so a waiter behind six cold listings gets an error instead of waiting without bound. Its rationale comment names the callers it serves now. AI-assisted-by: claude-opus-5-5 Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ List branches only when the switcher opens Opening a file ran the counted branch listing for every user, because the header decided whether to show its branch row from that listing. A cold listing runs a three-way comparison per diverged branch, and one save to main makes every branch of the file a cache miss again. The header now loads the branch count and the branch context with the file. The listing loads when the branch switcher or the all-branches dialog opens, and both show "Loading branches…" until it arrives instead of a list from an earlier opening. A failed listing used to read as an empty list, which on main hid the branch row and sent the shortcut to the create dialog. It now ends in an error state: the switcher and the dialog say "Couldn't load branches" with a Retry action, and the row stays, since its visibility comes from the count. A successful listing refreshes the count, so deleting the last branch from the list still hides the row. Creating a pull request from the branch banner looked the branch up in the listing to decide whether to save pending edits first. Without the listing loaded at open it would skip the save, so it now reads the branch context of the open file. AI-assisted-by: claude-opus-5-5 Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Show the branch row when the branch count fails A failed get-file-branch-count left no count, so on main the header hid its branch row and the shortcut opened the create dialog on a file that may have branches. The failure is now kept as an error state under :workspace-branch-count, and has-branches? reads it as true. The row then shows the Main chip and, where the status note goes, "Couldn't load branches" in the warning colour. Opening the switcher loads the listing through the existing loading and Retry path, and a successful listing settles the count, which replaces the note. AI-assisted-by: claude-opus-5-5 Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 📚 Name the callers of the branch listing limit again The merge of iop-vc-preview took its climit.edn, whose comment on :get-file-branches/global still described a panel open. Restore the comment this branch wrote: the listing runs when the switcher, the all-branches dialog or the dashboard popover opens, and a file open only counts branches. AI-assisted-by: claude-opus-5-5 Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 🐛 Mock the branch count in the library copy tests The header now shows its branch row from get-file-branch-count, not from the listing, so the library copy tests mock the count as well. AI-assisted-by: claude-opus-5-5 Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> --------- Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> |
||
|
|
6ddbfcf749
|
🐛 Make the backend branch tests pass on iop-vc-preview (#12568)
* 🐛 Give the branch concurrency limits a timeout Every global entry for branch and pull-request commands lacked a :timeout, so a saturated limiter parked its waiters forever and climit-test/example-config-bounds-every-wait failed on seven ids. Each entry now waits at most 60 s, the value of the other slow operations. The size gates in files_branch.clj refuse files whose operation would exceed 30 s, so the wait covers one in-flight operation with as much again in margin. AI-assisted-by: claude-opus-5-5 Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 🐛 Make the quota racer a member of the file team File access requires membership of the file's team since the permission check started joining team_profile_rel. The racer in create-checks-the-branch-quota-inside-the-lock held only a file role, so its create failed with :object-not-found and the test never raced. The racer now joins the file's team as an editor, so both creates again compete for the last branch slot. AI-assisted-by: claude-opus-5-5 Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> --------- Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> |
||
|
|
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 |
||
|
|
6ab7b3ebd6
|
Merge branch 'develop' into iop-vc-preview
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> |
||
|
|
0c0336da42 | Merge remote-tracking branch 'origin/staging' into develop | ||
|
|
eafa483a18 | Merge remote-tracking branch 'origin/main' into staging | ||
|
|
f242ff9574 |
🐛 Keep concurrency limits from deadlocking on nested calls
An instance left the audit events pipeline dead: every push-audit-events answered with 429 and 69 worker threads parked for good inside a limiter. Two code paths of one profile acquired the same two limiters in opposite order: a command declaring its own limits (upload-file-media-object) took process-image/by-profile and then root/by-profile, while a command with no limits of its own took the roots first and then re-entered climit from inside its handler with the same ids (create-file-media-object-from-url). Each path held what the other one waited for, and no limiter had a timeout to break the wait. Now wrap puts the root limiters after the command's own, so they end up outermost and every path meets them first: the nested chains carry only the command's own limits and run inside them, which puts both paths in the same order. impl-invoke also keeps, per thread, the set of limiters that thread is inside of and runs the handler inline when a call asks for one it already holds, so a handler can no longer queue behind itself. The shipped climit.edn sets a timeout on every limiter (60s for operations that can be slow, 30s otherwise), so no wait can park a thread for ever. The production file lives outside the repository and has to be mirrored. Tests cover the acquisition order, the deadlock, the reentrancy and the timeouts of the shipped config. AI-assisted-by: deepseek-v4.1-flash |
||
|
|
9abcfebf60
|
✨ Run binfile import and export as durable jobs (#12056)
* ✨ Run binfile import and export as durable jobs Import and export of `.penpot` files become durable jobs: two commands create them and answer at once with a job id the caller follows, while a worker of the `binfile` queue does the work. Each job keeps its own ledger (owner, params frozen at creation, result, progress history and an expiry), and the legacy commands and their SSE keep working. Why: both operations ran inside the request. A large import held it open for minutes, progress only existed while the caller kept the stream open, and a client that went away left work nobody recorded, retried or could cancel. The unified `job` substrate already dispatched, leased, retried and logged events, but it had no user-facing ledger. How: - The substrate gains the pieces a user-facing job needs: optional job-def metadata (family, resource-role), an expiry on submit, the owner in the runner context, and job resources in their own bucket, written with the profile of the job and never deduplicated. - One generic command per family: the caller names the job and sends its business params, the registry checks the family, the job-def decodes and validates, and permission and quota are checked first. - Progress taps of the core become milestones of the job: a shared stage vocabulary with self-contained counters, throttled heartbeats, and an interrupt that stops the run inside its own transaction on cancel. - Cancel reaches pending and running jobs from both modals, and closing a modal mid-run cancels the job it was following. - The export re-checks the read permission at run time and answers with the package descriptor; the import re-checks the edition permission, runs the core in a transaction so a cancellation rolls it back, and releases the consumed package and its temporary copy. AI-assisted-by: deepseek-v4.1-flash * ♻️ Handle externally-cancelled binfile jobs end to end The `cancelled` outcome of a job nobody followed no longer hangs its screens: the data layers emit an explicit terminal message for it. - The export emits `{:cancelled true}` per file and the modal paints a neutral cancelled row instead of success; a message without an artifact never reaches the download branch. - The import closes every entry with a dedicated cancelled message through the existing error rendering, so the wizard completes. - Both flows are pinned by data-layer tests, including the red state they replace. Also in this change: the progress-contract comment of the management API, early business-params validation in create-import-job (before assembling the upload), a `release-resource` helper without fabricated job ids, a note on the heartbeat write cost, and a memory cleanup. AI-assisted-by: muse-spark-1.3-contributor * ♻️ Pre-translate the cancelled import message The cancelled entry of the import wizard carried its message as a key for the UI to translate, which needed a rehash marker comment. Like its neighboring errors, it now translates eagerly in the data layer: the literal call keeps rehash clean with no marker, and the tests assert through the same call instead of the raw key. AI-assisted-by: muse-spark-1.3-contributor * ♻️ Move cancelled strings under generic labels keys The dashboard-specific cancelled key never shipped: before use it moves under `labels.*` next to the generic strings both flows already use, with Spanish translations following the established wording. AI-assisted-by: muse-spark-1.3-contributor * ✨ Add minor improvements * ♻️ Rename export job command to create-export-binfile-job Export now has one command per job type, the shape the :export-assets command needs: create-export-binfile-job freezes exactly the :export-binfile job, so :name no longer travels in the body and only :params does. Import keeps the generic envelope. submit-job reads the queue from the job-def metadata ::jobs/queue-name (falling back to :binfile), read by the command and never by the substrate, so a job-def can route its work to another queue without a change in the command. The rename reaches the frontend and the tests: the binfile export is asked for under its own name, and the exporter service keeps create-export-job. AI-assisted-by: deepseek-v4.1-flash * ✨ Polish the import and export status rows The status row of a file already carries what the file is doing, so the generic "Uploading file…"/"Downloading file…" lines are gone: the import wizard drops its own (the export one went with the jobs refactor), along with the now-unused style. In the export, every file is queued up front, even the ones whose job is created only after the previous one is over, and a file stays queued until its first milestone: a worker picking the job up no longer ends the queue. The queued text is a step smaller than the milestone. AI-assisted-by: deepseek-v4.1-flash * 🐛 Show the cancelled export and fix the import error The cancelled export was unreachable: mark-file-cancelled sets :loading false and the label lived inside the loading branch, so a cancelled file only painted a neutral notification. The terminal state now carries the label, and the export flags lose the `?` suffix (predicates keep it; a data prop is not a question). The import wizard rendered (:error entry) through tr, but every producer writes user-facing text (a hint or a translated label), never a key: it renders the text as-is now, and the dynamic-key ignores and the stale comment are gone. The total-text-limit comment no longer references the 200MiB it replaced; the value is 500MiB in develop already. AI-assisted-by: deepseek-v4.1-flash * ♻️ Reflect the real job params in the creation commands The creation commands declared their params as an open empty map, so the RPC contract and the generated docs said nothing about them. Each command now imports the schema from the job namespace (`export-binfile/schema:params`, `import-binfile/schema:create-params`), and the RPC validates the business params before the command runs. The job-def still owns the params and re-validates the assembled ones at run time. The commands no longer decode the params again: the RPC did it. Import joins export as one command per job type: `create-import-job` becomes `create-import-binfile-job`, its body no longer carries `:name` and the job-def is resolved by name like export's. Its creation schema keeps the version optional, because the command reads it from the package header when the caller does not say it, and fills the manifest metadata kept for audit. A version out of range is now refused by the schema (`:params-validation`) instead of the command's own `:unsupported-version` check, which is gone. AI-assisted-by: deepseek-v4.1-flash * 🐛 Stop the test system from running job runners The test system is main/system-config plus main/worker-config with a few components dissoc'ed. The runners of default, webhook and cron were listed by hand; the binfile runner added with the durable binfile jobs was not, so a real penpot/job-runner/binfile/0 thread ran during the whole suite. That runner races the tests that drive the loop by hand (run-batch plus run-worker-loop): it takes the job the test just dispatched and completes it, and the test then reads a state it did not produce. That is the reported flake of binfile-jobs-integration-test, whose middle assertion waits for a runner of another queue to not take the job. It only reproduced in CI: in the devenv PENPOT_TENANT makes the tenant baked into main/worker-config differ from the one the test builds, so the two used different queue keys locally. Drop every [<profile> :app.worker/runner] by key shape instead of listing the known queues, so a new queue cannot bring the race back. AI-assisted-by: deepseek-v4.1-flash * ✨ Upload the package one chunk at a time and as a percentage The upload milestone counts chunks, not units of work, so the row wrote "Uploading the package 3/9" over a file the user had uploaded once: it read as if nine packages were on their way. Write the stage as a percentage of the file instead ("Uploading file… (33%)"), and make the English text of the stage say file, not package. The chunks also go one at a time now (concat-all instead of merge-all with two in flight), so the percentage moves in a straight line instead of jumping. upload-blob-chunked is the generic uploader, so media and font uploads become sequential too. AI-assisted-by: deepseek-v4.1-flash * 🌐 Complete the Spanish catalogue es.po was 25 keys short of en.po: the 15 that the durable binfile jobs introduced (14 jobs.progress.stage.* and labels.queued) plus 10 gaps that predate them (errors.connection-error, errors.save-retrying, the find shortcuts, workspace.header.retrying, workspace.tokens.stroke-width and the mcp session labels). sync -l es then sorts the entries, syncs the #: comments from en and drops the 7 dashboard.import.progress.* keys en no longer has. The msgid sets of en and es are now identical (0 missing, 0 extra). AI-assisted-by: deepseek-v4.1-flash * ♻️ Move the job status keys into the jobs namespace The three status words the durable import and export rows render were filed under labels.*, next to the generic labels bucket, while their progress stages live under jobs.progress.stage.*: the strings of one feature sat in two places. Rename them so the whole job vocabulary is one namespace: labels.queued -> jobs.queued labels.export-cancelled -> jobs.export-cancelled labels.import-cancelled -> jobs.import-cancelled No other locale has these keys yet, so the rename costs no translation. The convention (a key names the feature that renders it; labels.* holds the generic reusable words) is written down in the frontend/translations memory. AI-assisted-by: deepseek-v4.1-flash * 🔥 Remove the translation keys nothing references Six keys of en.po (and of es.po, which now mirrors it) have no (tr ...) call site left in frontend/src or common/src, and no literal reference either: labels.uploading-file a duplicate of jobs.progress.stage.upload since the upload stage says "Uploading file…" labels.downloading-file dead since the durable jobs replaced the old import and export progress rows inspect.attributes.stroke.alignment.center inspect.attributes.stroke.alignment.inner inspect.attributes.stroke.alignment.outer workspace.toolbar.mcp-connect-here The first two are leftovers of the durable binfile jobs, the other four predate them. Both catalogues keep an identical msgid set afterwards. AI-assisted-by: deepseek-v4.1-flash |
||
|
|
8e62644741
|
🐛 Fix the open defects of the branching tech preview
Merge engine: a same-parent reorder, an addition under a parent main deleted, and a branch deleting a container main edited each lost work silently. Each now follows a policy read from backend/resources/app/branch-merge-policies.edn, re-read when the file changes, with the defaults: merge the reorder, and raise a conflict on the deleted parent or container. Added siblings keep the branch's order. A :mov-page index counts the target's full page list, a cleared page background reaches main, a per-attribute resolution can remove an attribute, and a token set reorder survives a rename on the other side. Conflict payloads name :main and :branch by document in both directions. Reorder detection costs nothing measurable on a page of 20,201 objects. Data versions: each op-log row records the data version it was written at, the branch replays its log in segments and migrates the document at each boundary, and the comparison base is migrated like main and the branch, so an upgrade no longer shows as false conflicts. Writers: a binfile import, an admin repair, file GC and a snapshot restore refuse a branch file, whose payload nothing reads, and the branch base pin cannot be renamed into a user snapshot. A branch save runs the ordinary media fix-up and logs the rewritten references. An update from main deletes the branch's copy of media main deleted and squashes no phantom media changes. Commands and UI: the pull-request commands publish their messages after the commit, the listing takes one connection per comparison and shows a warning instead of "in sync" when a comparison fails, materialize takes :branch-file-id, and a dashboard fetch that never answers ends in an error state instead of a spinner, with a refetch on reconnect. backend/dev/branch_merge_cases.clj builds one example file per merge policy case for demonstrating the alternatives. Two models wrote this change, claude-opus-5-5 and mimo-v2.6-pro, and the work switched between them, so the trailer reads mixed models. AI-assisted-by: mixed models |
||
|
|
81a605f11d
|
✨ Report what the listing and the branch open cost, and bound the concurrency
Three of the preview's paths reported no duration, and two of them were the slowest a user meets. A trial can be asked what a merge cost and could not be asked what the panel cost. The listing now passes through `audited` with two counters beside the duration, being how many open branches it compared and how many of those the summary cache answered, because a listing's cost is entirely explained by that ratio. The vector result was the one risk and it holds: a Clojure vector carries metadata, and nothing between the handler and the audit step rebuilds the value. A branch open now reports the derive's own time, its op-log depth, and its batch count, from `branch-file-data` itself. Depth is what predicts the cost, so the record carries it. An ordinary read never enters that function, so it gains no line, no prop, and no timer. A branch save gains the log line the other five print, and its timer moved rather than gaining a caveat. It now opens before `get-file`, so the number covers the read and, for a branch, the derive inside it, which is what compare, merge, update and materialize have always covered. The six figures are comparable for the first time. The concurrency limits were decoration. Thirteen ids were declared across the branch and pull-request commands and `climit.edn` configured none, so `climit/wrap` logged a warning at every start and ran each handler unwrapped, leaving the root chain's forty global permits as the only bound on an operation that holds three realized documents. Every declared id now has an entry, the two readers that realize whole documents gained a declaration, and each number is justified in a comment against `create-file-snapshot` as its anchor and marked provisional until a peak-heap measurement replaces it. The test helper drives the private save path directly, so it opens the timer the RPC method would have opened. Verified: 73 tests, 694 assertions, 0 failures across the three branch suites and `rpc-file-test`. |
||
|
|
2a5a235063
|
✨ Add the design preview of file branching and pull requests
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 |
||
|
|
b5fbc4fd8c
|
✨ Add size limits to profile props and plugin registry (#11596)
* ✨ Add size limits to profile props and plugin registry Bound the total serialized size of profile settings to 2 MiB (:profile-props-max-size), checked on the merged result before persisting, with a controlled :props-too-large error. Profiles that already exceed the limit can still shrink but cannot grow. Cap plugin registry entries in the shared schema (code 1 MiB, 50 plugins max, bounded name/host/description/icon) and restore rate limiting on the plugin RPCs (profile-mutations bucket, one write at a time per profile). The plugin manager now asks for confirmation before removal and ignores repeated clicks while a persist request is in flight. Closes #11592 AI-assisted-by: muse-spark-1.3-contributor * 🐛 Enforce plugin count cap, byte sizes and removal guard Enforce the declared 50-plugin cap in add-profile-plugin with a specific :too-many-plugins error (updates of existing entries still pass); the cap lives in a shared max-plugins constant. Measure profile props size in UTF-8 bytes instead of chars so multibyte content cannot slip past the limit. Cover install/remove persist logic with mocked-RPC frontend tests (release semantics, in-flight dedupe, validation vs rollback split) and add the missing boundary tests in common. Expose the in-flight persist set from the plugin registry and disable the remove button of entries being saved. Closes #11592 AI-assisted-by: muse-spark-1.3-contributor * 🐛 Fix rollback loops and restore paths in plugin registry Restore the previous plugin version instead of dropping the entry when a validation error rejects an update of an installed plugin. Make compensating writes one-shot with terminal callbacks so a persistent failure cannot ping-pong between install and remove. Restores keep the original list position; the unused public plugin-persisting? predicate is removed. Pin count-before-size precedence with a dedicated test and fix translation source refs to their canonical lines. Closes #11592 AI-assisted-by: muse-spark-1.3-contributor * 🐛 Guard notifications write and fix restore ordering Route update-profile-notifications through check-props-size! so oversized profiles cannot grow through that path; document the exempt system writers. Remove the duplicated stale entries in en.po, keeping the canonical translation refs. Restore rejected plugin updates at their original list position instead of leaving the optimistic move in place. Closes #11592 AI-assisted-by: muse-spark-1.3-contributor * 🐛 Skip no-op plugin removal and clarify size comments Return early from remove-profile-plugin when the id is absent: no wasted write, no size check, and no manufactured :plugins key that could spuriously fail on oversized profiles. Clarify that per-field string caps count chars while the byte budget is enforced by profile-props-max-size. Closes #11592 AI-assisted-by: muse-spark-1.3-contributor * 📎 Fix formatting in rlimit.edn for profile operations Signed-off-by: Andrey Antukh <niwi@niwi.nz> * 📎 Fix formatting of import-binfile/global entry Signed-off-by: Andrey Antukh <niwi@niwi.nz> * ♻️ Simplify props size check and tighten plugin entry caps Measure props with transit bytes directly instead of the PGobject string roundtrip. Rename check-props-size! to check-props-size: single hard limit on the merged props, no growth comparison, and return props so writers thread the check into the update. Move the 2 MiB default into default-props-max-size on the profile namespace, still overridable with the optional :profile-props-max-size config entry. Tighten registry-entry :code and :icon to 500 chars: they hold manifest paths, not content. Closes #11592 AI-assisted-by: muse-spark-1.3-contributor * 🐛 Fix compatibility problems --------- Signed-off-by: Andrey Antukh <niwi@niwi.nz> Co-authored-by: alonso.torres <alonso.torres@kaleidos.net> |
||
|
|
0255bed6c4
|
✨ Add deployment info to events (#11867)
* ✨ Add deployment info to events * ✨ Add get-environment-data RPC method Add a single public RPC method returning the deployment type and the enabled environment flags. It replaces get-deployment on the management API and get-enabled-flags on the main API. get-enabled-flags stays as a deprecated alias returning only the flags, so existing callers keep working until it is removed. The frontend event initialization now reads the flags from the new method. The exposed flags stay limited to audit-log and telemetry to avoid leaking internal backend flags. AI-assisted-by: deepseek-v4.1-flash --------- Co-authored-by: Andrey Antukh <niwi@niwi.nz> |
||
|
|
711ec04598
|
🐛 Fix renewal-email greeting falling back to the wrong name (#11848)
* 🐛 Fix renewal-email greeting falling back to the wrong name * 📎 Code review * 📎 Code review 2 |
||
|
|
643695b7fa
|
✨ Send notification email on password change (#11393)
* ✨ Send notification email on password change Add a password-changed email template and send it after every successful password change or first password set operation. This gives users a visible signal when a new authentication factor is established on their account. Closes #11392 AI-assisted-by: longcat-2.0 * 🐛 Address code review feedback on password notification - Send password-changed notification from recover-profile too (forgot-password reset path was missing the email). - Strengthen test assertions to verify email factory, recipient, and name via :call-args-list instead of just call-count. - Add negative test: no email sent when old-password is wrong. - Wrap pre-existing update-profile-password test with send! mock to keep its scope focused. Ref: PR #11393 AI-assisted-by: longcat-2.0 |
||
|
|
b660ea9d53 |
✨ Route the app by query string with screen key
Move SPA routing out of the URL fragment into the normal query string. The screen travels in a reserved `screen` key holding the route name (`?screen=workspace&team-id=…`); every other param keeps its name. `rt/nav` and `rt/resolve` keep their signatures. This deletes the fragment-mirroring URL surgery, simplifies link-preview (the server sees everything) and nginx (single path, no SPA fallback rules needed), and migrates OIDC redirects, email links, e2e helpers and plugin test utils to the new format. Legacy `#/…` URLs translate client-side for one Penpot version (`legacy-routes`, marked TODO(next-version)); non-SPA paths are untouched. AI-assisted-by: muse-spark-1.3-contributor |
||
|
|
a91d81c695 | 🎉 Add link preview metadata for shared links | ||
|
|
66b4a99ac3
|
🎉 Implement export jobs to process export requests (#11296)
* ✨ Add export job model, store and scheduler to exporter * ✨ Render wasm exports on pooled worker threads * ✨ Add export job REST API to exporter * ✨ Use export job API and allow cancelling wasm exports * 🔧 Show export jobs in the internal debug panel * 🔧 Pass flags and export job settings to the exporter container * 📚 Document the exporter job API and its redis layout |
||
|
|
292390f0eb | Merge remote-tracking branch 'origin/staging' into develop | ||
|
|
c01306734a
|
🐛 Fix default team and UI labels to use Personal Projects (#11418)
* 🐛 Fix default team and UI labels to use Personal Projects * 📎 Code review |
||
|
|
38004e6bb2
|
✨ Add the graph subsystem and graph visualization console to the backend (#11101)
* 🎉 Basic lbug connection for ingestion * ✨ Add Penpot-to-Ladybug graph ingest vertical slice * ✨ Use embedded Ladybug Java API instead of CLI * ♻️ Share Ladybug connection across ingest and stats * ✨ Validate graph ingest projections with Malli * ✨ Project nested shapes recursively into the graph * ⚡ Load graph ingest via Ladybug COPY bulk import * 🐛 Fix graph COPY ingest for multiline text names * ✨ Add Ladybug graph export to debug UI * ✨ Add debug graph console for in-memory Cypher queries * ✨ Add live file-change feed to debug graph console * ✨ Incrementally sync debug graph from Penpot file changes * ✨ Handle mov-objects in debug graph sync * 🐛 Fix batch delete sync and keep graph console feed alive * ♻️ Derive graph node schema from Malli registry * ✨ Add G6 graph view to debug graph console POC per work/g6/plan.md. New /dbg/actions/graph-data exports the in-memory Ladybug session as plain JSON (per-table node queries + multi-table IsChildOf match, row cap 100k with truncation flag). Console page renders it with AntV G6 v5 (jsDelivr CDN, antv-dagre BT layout, color+glyph per node table, validated palette) and refetches debounced on live :file-change messages. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 🐛 Fix list-column CSV ingest and serialize graph session access COPY failed on any file with container shapes: list-typed DDL columns (shapes UUID[], points STRING[], strokes JSON[], ...) were JSON-encoded in staging CSVs, which Ladybug's list parser rejects. Write Kuzu list literals instead, typed per column. Also: value->clj no longer crashes on LIST/STRUCT values (binding lacks value_get_value support; fall back to string), and the debug session Connection is now guarded by a per-session lock — it was shared unsynchronized between the msgbus sync loop and HTTP query/export handlers, and one lost DETACH DELETE was observed under concurrent refetch load. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Split graph console in two columns; add file tree and fullscreen Graph view moves to its own sticky right column (overrides .widget max-width). New /dbg/actions/graph-files endpoint lists teams -> projects -> files for the profile; the console renders it as a collapsible tree where clicking a file loads it. Maximize button fullscreens the graph panel and resizes G6 on fullscreenchange. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ♻️ Replace fullscreen with in-page expand for graph view Fullscreen API took over the whole output and broke window-manager splits (and is denied in some environments). The Expand button now toggles a fixed-position overlay covering the page while keeping browser chrome; Esc restores. Column positioning moved from inline style to the stylesheet so the expanded class can override it. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Fold containers as collapsible combos in graph view Non-empty containers (Page, Frame, Group, Boolean, SVGRaw) render as nested G6 rect combos holding their own node plus direct children; Document stays a plain node. Double-click folds/expands (collapse-expand behavior); collapsed combos show a member count and re-route child edges. Fold state is read back from getComboData and re-marked on every refetch, so it survives live redraws. Layout gains sortByCombo to keep same-rank nodes grouped by box. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ⚡ Fix graph view freeze on large files; add fold toggle and root rule Root cause of the tab freeze on ~1700-node files was G6's default entrance animation: measured 1700 nodes at >2 min animated vs 1.5 s with animation: false. Secondary cost was antv-dagre (~7 s at that size); since IsChildOf is a tree, an O(n) tidy layout (depth = rank, post-order leaf slots, parents centered) computed client-side replaces it and renders the same file in ~1.4 s. A guard skips auto-render above 4000 nodes with an explicit Render-anyway button, so opening the console with a huge session loaded stays responsive. Folding is now switchable ('fold containers' checkbox, persisted in localStorage) and generalized: any node with children folds except the IsChildOf root of the loaded graph, so Documents (and later Projects/Teams) fold automatically once they gain a parent node. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Add layout dropdown to graph view Adds a layout <select> next to the fold toggle, populated from the LAYOUTS map in the template: 'tree' (the O(n) preset layout, default) plus 13 G6 layouts (antv-dagre, dagre, circular, concentric, radial, grid, force, d3-force, force-atlas2, fruchterman, mds, combo-combined, random), all smoke-tested against combo data on this UMD build. Layout and fold toggle are independent; switching layouts recreates the graph instance (cheap with animation off); both choices persist in localStorage. antv-dagre stays available for when non-tree edges arrive. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Add query-result subgraph, ws auto-reconnect, adaptive animation The Cypher result pane now offers 'Show result in graph view': any UUID found in any result cell selects the matching nodes in the cached export and the view renders the induced subgraph (edges kept when both endpoints match); 'Show full graph' resets. No graph reconstruction from the query result is needed. The notifications websocket reconnects automatically (3 s retry) and resubscribes + refetches on reopen, so backend restarts no longer permanently kill the live feed; a lost session now reports 'no graph session (backend restarted?) - reload a file' instead of a bare 404. Animation is size-adaptive: graphs (or filtered subgraphs) up to 100 nodes render animated for didactics, larger ones stay animation-free; crossing the threshold recreates the instance like a layout switch. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Add graph toolbar, animate toggle, filter columns, repaint skip Graph view gains an on-canvas G6 toolbar (auto-fit, expand, restore - the fullscreen icons drive the existing in-page expand), an 'animate' checkbox that disables animation unconditionally when off (persisted, adaptive <=100-node rule applies only when on), and a ResizeObserver on the canvas so the panel follows window/flex resizes without touching the user's viewport. Preset tree positions are now only injected for the built-in tree layout, removing the tree-then-layout flash on animated re-renders under G6 layouts. Refetches skip the repaint when the display projection (nodes, edges, truncated) is byte-identical, so attribute-only change bursts no longer repaint. Console: default query returns s/t name+label over all edges plus filter_src_id/filter_tgt_id columns; filter_* columns are hidden from the results table (client and server render) but still feed the 'Show result in graph view' id harvest, keeping the table legible while the graph filter stays available. The query text persists in localStorage across page reloads (restored only over the default, never over a server-rendered query). Legend shows colored Unicode glyphs matching node shapes instead of squares with textual annotations. Load/Unload buttons share one row (HTML5 form attribute), and the loaded file name links to the Penpot workspace via the legacy /#/workspace/<project-id>/<file-id> route resolved client-side from the files-tree payload. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 🐛 Fix runaway graph panel growth and blank canvas; drop Expand button Root cause of 'graph flashes on load then disappears' plus unbounded horizontal growth of the graph panel: fieldsets default to min-inline-size: min-content, so #graph-view-panel sized to its content, and the new ResizeObserver->setSize path closed a feedback loop (setSize -> slightly wider G6 canvas -> wider fieldset -> wider .dashboard flex column -> observer fires) that grew the page ~10px per frame and wiped the painted canvas on every step. Fix severs the feedback path: #graph-view-panel gets min-inline-size: 0, #graph-canvas gets overflow: hidden, and the page section gets flex: 1 1 0 with min-width: 0 so column widths are viewport-driven, never content-driven. This also fixes the original narrow-window scrollbars defect for real. The observer stays (guarded by a current-size comparison) because G6's autoResize is inert on this UMD build (verified: window resizes left the canvas size untouched); the inert autoResize flag is dropped. Legend items now join with spaces so the nowrap spans can wrap between entries. Also removes the header Expand button - the toolbar's expand/exit icons cover it, Esc still restores. Verified against the running devenv with a logged-in profile and variants_simple loaded: graph renders and persists, widths stable over multiple seconds at 1400px and 1000px viewports with no horizontal overflow, canvas follows both window shrink and grow, toolbar expand gives a full-page canvas and Esc restores. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Make the default query self-explanatory; link the Cypher docs The default query is now multi-line with // comments that explain the filter_* column convention in place (Kuzu accepts comments and blank lines mid-statement; verified against an in-memory database through the console query path). The query fieldset is retitled 'LadybugDB Cypher' with the Cypher word linking to https://docs.ladybugdb.com/cypher/. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 📎 Fix linter issues * ✨ Add Component nodes and IsInstanceOf edges * 🐛 Fix memory leak * ✨ Style Component nodes and IsInstanceOf edges in graph console Slice-3 export sends edges with a rel field. Derive tree ranking, combo derivation and fold-ability from IsChildOf only; draw other rels as overlay edges with per-rel styles (EDGE_STYLES: IsInstanceOf violet dashed, matching the new Component diamond in NODE_STYLES). Legend now lists only displayed node tables and rels, re-rendered per redraw; help text trimmed to essentials. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Add graph diff marks with step fade to graph console Each display-changing refetch is a step: added nodes/edges get a green halo, removed ones stay as ghosts with a dashed crimson halo (nodes, fading opacity) or thicker crimson stroke (edges), re-entering layout and combos through their ghost IsChildOf edges. Marks fade linearly and drop after N steps; N is the new "fade" number input (localStorage, 0 = off). Dash + fade carry the added/removed distinction under red-green CVD (#40c057/#c2255c, deutan dE 17.4); diff is vs the previous display step, not arbitrary revisions. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 💄 Reserve chroma for changes in graph console diff mode Monochrome entity scheme: all node tables share one slate hue, lightness separates within-glyph siblings (validated, worst pair dE 17.5), SVGRaw becomes the hollow hexagon, both rels go grey with dash as the only separator. Diff marks now own all color: thick green/crimson stroke ring (dashed for removals) plus a larger, subtler halo; the legend gains +/- entries while marks are live. Two additions to guide the eye: a brief DOM-overlay pulse on age-0 elements (independent of the G6 animation gate) and a "fold unchanged" toggle that collapses every combo not on an ancestor path of a changed element. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 🐛 Expand folded combos that gain changed elements setData merges datum props by id on a live G6 instance, so omitting style.collapsed retained a previous true: with "fold unchanged" on, a change inside a folded combo pulsed but never expanded it. Write the boolean explicitly both ways. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Label edge rels with compact unicode symbols Dash variants alone cannot carry the growing rel roster: EDGE_STYLES entries gain a sym rendered as a small mid-edge label with a white backing (IsInstanceOf = "∈"; IsChildOf stays unlabeled as the background structure), and the legend shows the symbol. Convention from the abacus viewer EDGE_SYM dict. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Add node inspector panel to graph console Clicking a node fetches its full attribute row (MATCH (n:`Table` {id: uuid(...)}) RETURN n.*) through the query endpoint and renders non-null attrs into a panel under the canvas (count of empty attrs noted). Panel over tooltip: projected tables carry ~80 columns, and the panel persists for reading without obstructing the graph. Table/id are validated before Cypher interpolation; the listener is re-attached on every instance recreation. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 💄 Graph console QoL round "Show result in graph view" moves into an actions bar above the results table; results scroll inside a 45vh container (client and server render paths); the Loaded-session fieldset gains a live "Graph size" line that stays fresh through skipped repaints; IsInstanceOf mid-edge label becomes the spelled-out rel name (∈ read as membership, not derivation) with the legend falling back to the dash-arrow for long syms. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Sync Component library changes into the Ladybug graph * 💄 Polish graph console session panel and edge labels Loaded-session fieldset: graph size gains a resident-memory estimate (fit to graph_sizes.md: ~1.1 MiB floor + ~5.4 KiB/node) with per-table counts on hover, replacing the load-time Projection stats; loaded-at compacts to local HH:MM with the full instant on hover. Edge rel labels drop to 7 px and lose the dashed stroke — the text label alone carries rel identity. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Add PNG export and hover tooltips to graph console Toolbar gains an export item: graph.toDataURL({mode: "overall"}) downloads the whole laid-out graph as graph-<revn>.png — page-chrome-free captures, also the fast path for agents debugging the console. A hover tooltip (table, label, id) backs the reduced/absent labels on dense layouts. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 💄 Prune graph console layout roster and tune overlap Remove grid, random, force, fruchterman, force-atlas2 (nothing over the kept set) and mds (stress layout degenerates to spokes on tree distances, no collision term to tune). Parameterize the keepers against node overlap — concentric/radial get preventOverlap+nodeSize, d3-force a collide radius — and shrink node labels to 7 px on those layouts (DENSE_LABEL_LAYOUTS), verified against variants_simple (72 nodes). Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Highlight clicked node neighborhood in graph console click-select behavior with degree 1: the clicked element keeps a black ring, direct neighbors stay full-strength, everything else dims to 0.2 opacity (inactive state); clicking empty canvas clears. Works on edges too (selects both endpoints) and composes with the node inspector on the same click. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Add overview mode: fold containers at or beyond a depth "fold >= depth" number input (root = 0, empty = off, localStorage): every combo whose container sits at that IsChildOf depth or deeper collapses, giving a top-of-file overview (e.g. 2 folds the containers hanging from a Page). Composes with fold-unchanged — depth folds first, changed ancestor paths are then drilled open. Derived fold state overrides manual folds while active. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 🐛 Restore fold-containers as the combo master gate Since fold-unchanged and depth folding arrived, withCombos ORed them in, so unchecking "fold containers" could no longer remove the combo boxes. The checkbox is the gate again; the derived fold rules are dormant without it. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 💄 Console UI polish round Merge the load form and files tree into one "Load graph from Penpot" box (tree first, uuid + Load/Unload in a row); Loaded session carries HH:MM in its legend; the Live changes box stays hidden until the first change arrives; query fieldset reads "Query graph (LadybugDB Cypher)" with the link covering both terms. Drop the hover tooltips (distracting, useless zoomed out) and the resident-size estimate (per-table counts stay on hover); every toggle gets a "When set/checked ..." title. Depth fold: 0 now expands every container (no more hunting for max depth). Node inspector: two-column flow, structured or long values folded behind the file-tree disclosure triangle. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Report actual graph memory from the buffer manager graph-data gains bm-bytes (CALL bm_info() -> [mem_limit mem_usage], nil-safe, under the session lock); the session panel shows it as MiB behind the node/edge counts — real resident memory replacing the removed estimate. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 💄 Console control-bar and session-panel rework Left column narrowed 440->330 px (uuid input flexes). Control bar reordered: layout first, then animate and fade (narrow inputs), then the fold set; "fold containers" renamed "foldable containers" (on = foldable, not folded). Load becomes Reload once a session exists (same operation as the removed Full-reload button — load-session! on the current id; tooltip explains the fallback role) with Unload beside it. Session panel: revisions on one line ("ingested at N · graph now M", hover explains the difference), duplicate uuid after the file name dropped. Tried and rejected: fishbone (no positions on graph data) and compact-box (G6 tree layouts walk parent->child, IsChildOf points child->parent). Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Legend entries toggle node-table visibility Clicking a legend entry hides/shows that table across the view (struck-through while hidden, kept listed for re-enabling; pure client-side id filter through filteredGraphData, edges drop with their endpoints, ghosts respect it). Also: setting fold >= depth above 0 now switches foldable containers on — a positive depth was silently inert without combos. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * ✨ Enable the edge-bundling plugin Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 💄 Session breadcrumb, changelog colors, spacing File line becomes team › project › file (clickable) with the resident-memory figure beside it (moved up from the graph-size line; breadcrumb resolves from the files-tree payload, so files outside the profiles teams show plain). add-obj/del-obj in Live changes wear the canvas diff colors. Paragraph margins tightened above Feed; left column 330→350 px. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 🐛 Guard renders against heavy graphs; add ?safe escape hatch A heavy file could freeze the tab on load-and-render despite the animation gate: the render guard counted nodes only, and the edge-bundling plugin is iteration-heavy in edges. Guard now also trips on edges (8000), edge bundling only activates at <= 300 edges, and /dbg/graph?safe disables auto-render entirely (counts + "Render anyway"), so a page that hung can always be re-entered with the session intact. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 🔥 Remove the edge-bundling plugin Bundled edges render unsmooth and ugly on this build; the gating constant goes with it. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 💄 One row per operation in the Live changes table Columns revn | op | id: the revn repeats across a batch, the op wears the canvas diff colors (shape/attrs detail on hover), and the id column shows the uuid last group with the full uuid on hover, or N/A for ops without a subject id (e.g. mov-objects). Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> * 🐛 Use app.system/system in the graph ingest helper develop renamed app.main/system to app.system/system and dropped the app.main require while this branch was away. Rebasing replays the old call, so clj-kondo reports an unresolved namespace and the ns will not load. * ✨ Put the graph subsystem behind a flag, off by default (#11075) `app.graph.ladybug` imports `com.ladybugdb.*` at namespace load. Two namespaces reach the subsystem and both required it at the top level: `app.http.debug`, which registers the `/dbg` routes, and `app.srepl.main`, which loads with the REPL server. Every backend built from this branch therefore linked the Ladybug native library into the JVM at boot, whether or not a graph was ever used. Add a `:graph` flag to `varia`, deliberately absent from `default` so that a released Penpot ships with the subsystem off. Both require sites now resolve `app.graph.*` at call time, so with the flag off no `com.ladybugdb` class is loaded. The nine `/dbg` graph routes are registered only when the flag is on, and 404 otherwise. The `/dbg` admin gate is untouched: the flag decides which routes exist, not who may reach them. When the flag is on, route init requires the subsystem eagerly, so a missing or unusable native library fails the boot rather than the first console request. No tracked file turns the flag on. `backend/scripts/_env` leaves it out, so a devenv boots with the subsystem off exactly as a released build does, and `docker/images/docker-compose.yaml`, the self-hosting distribution, is untouched. Whoever works on the graph turns it on for one checkout through the gitignored `backend/scripts/_env.local`, which every backend and exporter dev script sources right after `_env`. Verified with `-verbose:class` over a boot's namespace load plus `ig/init-key ::routes`: 9 `com.ladybugdb` classes before this change with no flag set, 0 after it with the flag off, 9 with `enable-graph`. * ⬆️ Take Ladybug 0.19.1 `com.ladybugdb/lbug` moves from 0.18.0 to 0.19.1, the current release on Maven Central. The engine fixes a SIGSEGV on an unwrapped parameter and moves parameter coercion out of JNI, so shipping 0.18.0 would land a native library into `develop` with a known crash already fixed upstream. Nothing else changes. This branch has no `app.graph.arrow`, so the top-level Arrow field-name backticking that 0.19.x retires does not exist here and there is no workaround to remove alongside the bump. AI-assisted-by: mixed models * 📎 Pin the graph console's G6 bundle to an exact version The console loaded `@antv/g6@5` from jsDelivr, a floating major range, so the JavaScript served into the page could change without a Penpot release. Pin it to 5.1.1, the version the range resolves to today. Where the dependency finally belongs is an open question for review: vendored into `backend/resources`, declared in `frontend/package.json` if the console moves out of `/dbg`, or left on the CDN. Pinning removes the floating-code problem without pre-empting that decision. AI-assisted-by: mixed models * ✨ Add graph provenance, column naming and two transforms A projected graph is a cache of one file at one revision, built by one schema, and nothing in it said so. `GraphMeta` records the file, the revision, the schema version and the producer, and is written last, so its presence also marks the build complete and its contents say whether a cached database is still worth opening. - `graph/meta.clj`: the `GraphMeta` table and its writer. - `graph/schema/contract.clj`: one place that maps a Penpot key to its graph column. The rule is snake_case of the key; every exception, be it a rename, a drop or a type override, is recorded there with its reason, so a divergence is a diff to review rather than a silent rename. - `graph/project/document.clj`: `page-id` and the inherited `component-id` are written during the tree walk, which already knows both, rather than by a post-ingest statement. `graph/sync.clj` does the same on the incremental path, so a live-synced graph matches a rebuild. - `graph/project/transforms.clj`: a registry, so adding a derived-link pass is one entry. Adds `RefersTo` (from `shape-ref`) and `FillsSwapSlot` (from `swap-slot-*` entries in `touched`, then stripped as `ctk/normal-touched-groups` does). - `graph/debug.clj`, `graph/stats.clj`: enumerate relationship tables from the catalog instead of naming them, so the console's graph view and the ingest counts pick up new edge types without being told. - `graph/debug.clj`, `http/debug.clj`: `graph-export` gains `source=session`, which snapshots the live in-memory console graph through EXPORT/IMPORT DATABASE. Live sync moves that graph away from a fresh projection, and taking it away to query elsewhere is the point of asking for it. AI-assisted-by: mixed models * 🐛 Write graph values Ladybug's CSV reader cannot carry through Cypher Three parity failures against beadpot's suite, all one cause: the bulk loader put compound and multi-line values into CSV, where Ladybug parses a field's *contents* as a literal with no escape mechanism at all. Verified against 0.18: a comma inside a list element ends the element, quotes are kept as part of the value rather than delimiting it, and the parallel reader rejects quoted newlines outright. So a value now goes through CSV only if it cannot be misread there — UUIDs, numbers, booleans, single-line strings, and lists of those. Everything else (MAP, STRUCT, STRING[]/JSON[], any string containing a newline) is written after the COPY by one Cypher statement per row, where `app.graph.ladybug` escapes properly. Parquet removes the distinction entirely and is still the right destination (masterplan P0 T1); this is what CSV can honestly do. Consequences beyond the encoding: - `touched` entries reached the graph as `:swap-slot-…`, keywords stringified with their colon, so `LinkSwapSlots` matched nothing. Keywords now render through `name`. - Shape names lost their newlines to a flattening step that existed only to keep the CSV writer happy. They are preserved. - `applied_tokens` keys are rendered camelCase, the form Penpot's own JSON encoder produces and the one beadpot's `AppliedTokenKey` holds — a MAP column's keys are values, not schema, so they are not snake_cased. - `link-component-instances!` keys on `component-file`, not `component-id` alone. The projection denormalizes `component-id` down the shape tree, after which it no longer tells an instance head from a shape inside one, and the transform linked every descendant frame; `ctk/instance-of?` requires both keys anyway. IsInstanceOf on the variants fixture: 78 -> 60, matching beadpot exactly. `app.graph.schema.nodes/format-column-value` is now the single place that knows a column's type and its contract details, used by the bulk loader and the incremental sync alike so the two cannot disagree about a value's shape. * ✨ Type graph columns as tightly as Ladybug allows Ladybug is schema-first and strongly typed: a property key gets its type at table-creation time and there is no widening later. That makes the Malli to Ladybug mapping the whole of the graph's typing, and it was leaving a lot on the table: a transform stored as `STRING`, a rect as `JSON`, a set of feature flags as a single `STRING`. A column typed `DOUBLE[4]` is four numbers a consumer reads as a tensor row; the same value as JSON is text somebody has to parse and trust. `app.graph.schema.types` now maps, in order: scalars; Penpot value types whose layout is fixed even though Malli only sees a map or a string (`::gmt/matrix` to `DOUBLE[6]`, `::gpt/point` to `DOUBLE[2]`, `::grc/rect` to `DOUBLE[4]`, `::clr/hex-color` to `UINT32`); then structure, with collections to `T[]`, `:map-of` to `MAP(k, v)`, and a closed map of scalars to a `STRUCT`. JSON is the fallback of last resort, for schemas that genuinely admit more than one shape. Two defects fell out. `::sm/set` was unmapped, so `features` and `migrations` were single strings rather than `STRING[]`, and `::sm/one-of`, how Penpot spells a closed set of keywords, was unmapped too, so `blend-mode`, `grow-type`, the constraints and every `layout-*` were mistyped. A tight column is only worth having if the writer fills it in that shape, so `app.graph.schema.values` shapes a value for its type: a matrix record into six doubles, a hex colour into a packed integer, a map into a struct's fields. Both writers go through it, so the bulk load and the incremental sync cannot disagree. What that required: - STRUCT field names must be backticked in the DDL *and* in every literal, because a grid cell has a field named `column`. The catalog reports them bare. - A struct literal's type is its field list, so every declared field must appear, and an absent one needs `cast(NULL, '<type>')`. A bare NULL is typed STRING and changes the struct's type. - `STRUCT(…)[]` starts with `STRUCT(` but is a list, so the list check comes first. - Nested lists cannot be rendered with `str`: Clojure's `[1 2]` is space-separated and Ladybug reads it as a one-element array. Three more corrections in the same area: - `project-attrs` used truthiness where it meant `some?`, so `opacity 0` and `blocked false` projected as absent. - Set-valued columns are written sorted. A set has no order, so the column varied between builds of the same file, which is precisely what stops two builds being diffable. - An empty collection is written as `[]` rather than skipped. A shape with no fills has none; NULL would say "unknown". Renamed the `kuzu-*` helpers to `ladybug-*`: Kùzu is deprecated and Ladybug substitutes it, so a name bearing the engine should bear this one. The one remaining mention cites the upstream issue Ladybug inherits. AI-assisted-by: mixed models * ✨ Add the file-level graph columns and tighten the svg ones Split out of "🐛 Declare the shape attributes stored files carry", which is now #11125 and carries only its `common/` half. This commit is the graph's own side of that change, and it stays on this branch. `app.graph.schema.contract` pins `svg_viewbox` to `DOUBLE[4]` and `svg_transform` to `DOUBLE[6]`. The shape schema types both `:map` on purpose, because legacy files hold them as plain maps rather than as `::grc/rect` and `::gmt/matrix` records, and a tighter *schema* would reject those files. A tighter *column* costs nothing, since `app.graph.schema.values/coerce` reads either form. `app.graph.schema.nodes` declares four file-level attributes as projection `:extra` rather than in `ctf/schema:file`: `:options`, `:backend`, `:comment-thread-seqn`, and `:ignore-sync-until`. Declaring them in the file schema breaks saving, measured at 185 failures, because `app.binfile.common/update-file!` derives its UPDATE column list from a file map's keys and the `file` table has no `backend` column, that value being synthesized on read. An `:extra` is local to the graph and cannot reach a write. `app.graph.project.document` lifts `:options` out of `:data` before the blob is dropped, so a consumer reads file-level configuration without opening the blob. AI-assisted-by: mixed models * ✨ Add the Arrow prerequisites for in-memory bulk load lbug pulls arrow-memory-core and arrow-vector but no allocation-manager implementation, so RootAllocator cannot be constructed; arrow-memory-netty 18.2.0 matches the arrow-vector lbug already brings and pulls only netty-buffer, netty-common, jackson and slf4j-api, all of which the backend already has. --add-opens=java.base/java.nio=ALL-UNNAMED is the second half: without it MemoryUtil's static initializer dies with an InaccessibleObjectException that surfaces as an unhelpful NoClassDefFoundError from anything touching RootAllocator. It has to be present at JVM start, hence all three places. Note app.main/restart will not pick it up — it restarts integrant inside the same JVM, so the process must be restarted. Worth a reviewer's attention: this is a JVM-wide flag added for one subsystem. It is the standard Arrow requirement and grants nothing beyond reflective access to java.nio, but it strengthens the case for putting the whole graph subsystem behind a feature flag. * ✨ Bulk load through in-memory Arrow; delete the CSV loader app.graph.arrow stages rows as Arrow VectorSchemaRoots and COPYs from them. No file is written at any point and no value is rendered as text for the engine to re-parse, so the defect class that produced three of this branch's four backend defects cannot recur. app.graph.bulk is deleted whole. csv-representable?, defer-to-cypher?, multiline?, fixup-statements, ladybug-literal, ladybug-list-element, ladybug-list-cell and staging-dir go with it, along with the post-COPY Cypher pass that emitted one SET per row. Measured before deciding: the fixup pass was ~77% execution, 16-22% parse and 6-7% round-trip, and prepared statements could not have recovered any of it — every fixup row carries a MAP column and Ladybug binds scalars only. So this replaces rather than optimizes. Marginal ingest 4.0 -> 1.21 ms/shape; ~25 s extrapolated at 20k shapes against the ~2 min the CSV path projected. Size unchanged. Four engine facts the implementation rests on, each verified against 0.18.2 with a standalone probe: - An Arrow table is not a COPY source identifier but is a MATCH-able node label. - A MAP vector's entries child must be a non-nullable struct, and MapVector.getWriter promotes it to a sparse union, so map vectors are built from an explicit Field and filled child-first. - Ladybug names a staged table's columns and struct fields from the Arrow field names and quotes none of them, so anything needing quotes must arrive quoted — hence cypher-property-key, not column-name, names the Arrow fields. - createArrowRelTable cannot resolve endpoints against a UUID-keyed node table under any encoding, so edges stage as a node table and the COPY subquery joins them. values/coerce is reused unchanged, so the Arrow and Cypher writers cannot disagree about a value's shape; nodes/column-map-key-fn is extracted so they cannot disagree about a MAP's key spelling either. Verified with pytest --graph-origin=penpot-only unchanged at 225/38/1 and --graph-origin=penpot unchanged at 258 passed / 2 pre-existing failures, both baselines re-established against a reverted backend rather than assumed; with bp graph diff between a CSV-built and an Arrow-built graph reporting "Graphs agree"; and with an adversarial round-trip carrying a quote, a backslash, a newline, a CRLF and a tab through STRING, STRING[] elements and MAP values. The diff was necessary, not belt-and-braces: both parity suites passed an earlier revision of this change that was writing EDN into every JSON column, because beadpot's assertions never parse those columns. It also showed Arrow correcting a CSV defect — an empty Component.path was being stored as NULL, because Ladybug's CSV reader cannot distinguish an empty field from an absent one. * ✨ Add a prepared-statement surface to the graph connection `app.graph.ladybug` could only run Cypher as text. Every value the sync path writes is therefore concatenated into the statement, and nothing can ask the engine whether a statement is even valid without running it. Add the four functions that close both gaps. `prepare-on-connection!` parses and binds without executing. `execute-prepared!` binds a parameter map and runs it. `exec-prepared-on-connection!` prepares every statement in a batch before executing any of them, so a parse or bind failure aborts before the first mutation. `validate-on-connection!` returns `{:ok? :error :read-only?}` instead of raising, which is what a gate wants. `->param-value` is the only `Value` constructor on the write path. It is unconditional: on lbug 0.18.2 an unwrapped parameter does not raise, it SIGSEGVs the JVM inside `lbug_value_clone`. Parameters are scalars only, because the JNI `Value` constructor takes no list or map, so `MAP`, `STRUCT` and `T[]` columns stay literal-rendered and the `:else` branch raises rather than crashing. Two departures from the design, both closing a JNI-handle leak on the error path: `prepare-on-connection!` closes the failed `PreparedStatement` before raising, and `execute-prepared!` closes every `Value` it built, including the ones built before a later parameter was rejected. `as-statement` accepts a bare string, so the sync builders can convert to bound parameters one family at a time rather than in one commit. AI-assisted-by: mixed models * 🐛 Write the document revision to the column that exists `set-document-revision-statement` emitted `SET d.revn`, but the column is `revision`: the beadpot contract renames `:revn` and the DDL has followed it since. The statement is the last one in every sync batch, so each batch raised after its mutations had already committed, and the session's in-memory index stayed frozen at its load-time revision. Name the column through `nodes/cypher-property-key` rather than spelling it, so the DDL and the statement cannot disagree again. Found by the binder gate in the next commit, on its first run. AI-assisted-by: mixed models * ✨ Gate every sync statement template through the binder Nothing checked that the eleven Cypher templates `app.graph.sync` emits still bind against the DDL the schema registry generates. A renamed column, a dropped table or a reserved word emitted unquoted surfaced only when a live session ran the statement, and by then the batch's earlier mutations had committed. `backend-tests.graph-binder-gate-test` opens a `:memory:` database, creates the live schema on it, and *prepares* one instance of each template without executing any of them. 14 tests, 51 assertions: the eleven templates, label coverage over all twelve registered node tables, and two assertions on the gate itself, that a `RETURN` reads as read-only and a `SET` does not, and that an unbindable statement is reported rather than thrown. It was not green on HEAD: it caught `set-document-revision-statement` writing a column that no longer exists, fixed in the previous commit. Red on both injected templates tried. No `:jvm-opts` change: CI's `-M:dev:test` already carries the native access flags the engine needs. AI-assisted-by: mixed models * 🐛 Let the engine quote the Arrow field names it interpolates `node-batch` named every top-level Arrow field with backticks, so that a column whose name is a reserved word (`Page.index`, `Document.options`) survived the DDL Ladybug generates for a staged table. The engine now quotes those identifiers itself, and it does not collapse a doubled backtick, so a pre-quoted name reaches the parser as ``index`` and `createArrowTable` fails outright: Parser exception: mismatched input '``' expecting PRIMARY Name the fields with `column-name`. The `COPY` projection is Cypher rather than DDL and keeps its own backticks through `cypher-property-key`, and STRUCT member names keep theirs too: those come out of `LogicalType::toString()`, which the DDL builder does not touch, so an unquoted member called `column` still fails to parse. Measured with `probes/arrow/probe25.clj` against lbug 0.19.1: a plain top-level reserved word loads and reads back, a pre-quoted one fails to parse, a plain STRUCT member fails to parse, and a pre-quoted one loads and reads back. Also re-dates the engine facts in the `app.graph.arrow` docstring to the version they were checked against, drops the SIGSEGV note from `->param-value` now that `Connection.execute` rejects an unwrapped parameter, and removes two references to the CSV loader. AI-assisted-by: mixed models * 📚 State what the graph schema does, not what it mirrors The graph namespaces explained themselves by citing a separate project whose Python pipeline reads the graphs this backend writes. A reader of this repository does not have that project and should not need it, and a docstring that justifies a choice by pointing elsewhere cannot be checked here. Every claim survives; only the framing changes. Column names and types are Penpot's own decision, recorded with the reason for each divergence from the snake_case default. The transform registry describes the edges it materializes. The denormalizations in `app.graph.project.document` are justified by the walk already holding both answers. Three corrections fall out of the rewrite: - `app.graph.schema.contract` claimed a test, `graph_contract_test`, that walks a checked-in schema manifest and fails on any divergence. No such test exists. The paragraph is gone. - `app.graph.project.document` pointed at `app.graph.meta/projection-transforms`, which does not exist. - `app.graph.project.transforms/registry` claimed its entries were "in application order" while `apply-transforms!` reduced over the literal vector. The three registered transforms read disjoint columns, so the order is not load-bearing. The docstring now says so, and the one real ordering constraint is stated where it applies: `link-swap-slots!` strips `swap-slot-*` entries from `touched`, so anything reading `touched` has to run before it. `contract/pending-beadpot-columns` becomes `contract/unprojected-keys`. It is referenced nowhere else. AI-assisted-by: mixed models * ✨ Refuse a mutating query from the graph console `debug/query-session!` ran whatever it was handed against the session connection. A session graph is a projection of a file, rebuilt from that file by Reload, so a mutation from the console produces a graph no rebuild reproduces and no query result explains. Bind the statement against the live schema first. A statement that does not bind reports the binder's own message and executes nothing, which also turns a misspelt table or property into an immediate error instead of an empty result. A statement that binds runs only when the engine's own read/write analysis calls it read-only. The console's query box is labelled read-only. Load, Reload, Unload and live sync are unaffected: they are separate handlers and do not go through this path. AI-assisted-by: mixed models * 🐛 Keep a synced graph equal to a rebuilt one Cold projection and incremental sync are two implementations of one mapping and nothing checked that they agree. They did not. `backend-tests.graph-sync-parity-test` projects a file into one `:memory:` database, applies a change list to that database and the same list to the file data, projects the result into a second database, and diffs the two down to the row and the column. It found four disagreements, each fixed here. **Sibling order was inverted.** A container's stored `:shapes` list runs bottom to top and `IsChildOf.position` numbers children in Penpot z-order, so appending to the list means taking position 0 and pushing every sibling up. Sync instead handed each new child the next free number, so any container edited live carried its children in the opposite order to a rebuild, and a delete left a gap where a rebuild renumbers densely. `insert-position` and `renumber-siblings` put the two paths on the same rule for `:add-obj`, `:mov-objects` and `:del-obj`, including a block move and `:after-shape`. **A moved shape kept its old parent.** `:mov-objects` moved the edge and left the shape's own `parent_id` and `frame_id` columns pointing at the container it came from. Both now follow, and `frame_id` follows through the whole subtree the shape carries, as `app.common.files.changes` does for `:mov-objects`. A top-level shape's column holds `uuid/zero`, the page's root frame, while its edge points at the Page. **A container's `shapes` column went stale.** Nothing maintained it after an add, a move or a delete. It is now rebuilt from the sibling order on every change that touches a container. **Pages came out backwards.** `projection-data` reversed `:pages` before numbering them, which is right for child shapes and wrong for pages: `:pages` is the tab order and has no second ordering to undo. `Page.index` and the page's `IsChildOf.position` are now that order. One defect the test does not reach, fixed on the way past: `index-add-shape!` accepted `:component-ctx` and dropped it, so a shape added under an instance head added in the same session inherited no `component-id`. AI-assisted-by: mixed models * 🐛 Build a synced page node the way the projection does `apply-add-page` sent the new Page node through `nodes/validate-node`, which checks a map against the registry schema and returns it unchanged. Every other node on both write paths goes through `nodes/project-attrs`, which also selects the projected keys and is the single place a column-level rule can live. A rule added there reached a rebuilt page and not a synced one. AI-assisted-by: mixed models * 🐛 Let the graph view's query filter follow the graph "Show result in graph view" froze the set of node ids the query returned and filtered every later repaint against it. Live sync creates ids the set has never seen, so a shape created while a filter was on could not appear in the view at any point, and clicking "Show full graph" was the only way to see it. A node the query would no longer match stayed. Keep the query beside the ids and re-run it whenever the graph repaints, which is only when the projection actually changed. A failed re-run keeps the ids in hand and says so on the status line rather than passing a stale view off as current. `idsInResult` and `presentIds` are extracted from the two places that scraped UUIDs out of a result. Verified in the devenv: with a filter showing 108 of 276 nodes, a `:file-change` adding a Frame published on the session's msgbus topic took the view to 109 of 277, with the new node carrying its added mark, and no interaction. AI-assisted-by: mixed models * ♻️ Rename app.graph.project to app.graph.projection `project` is a Penpot noun: a team holds projects and a project holds files, and the graph will carry a `Project` node table. A namespace called `app.graph.project.document` therefore reads as "the graph of a Penpot project" and means the opposite. `projection` is the word the rest of the subsystem already uses for the operation: `projection-data`, `load-projection!`, `:projection` in the ingest report, and `app.graph.schema.projection`. Pure rename. Both namespaces and every alias move; nothing else changes. AI-assisted-by: mixed models * 📎 Apply the project formatter to the graph namespaces `cljfmt check src/ test/` is a step of the Backend workflow and these two files did not pass it: an import block sorted the way a human reads it rather than the way the formatter sorts it, and a `cond` in `format-typed-value` indented one column short. Formatter output only. No semantic change. AI-assisted-by: mixed models * 📚 Document graph experiment architecture Add Serena memory coverage for the embedded Ladybug graph subsystem.\nDocument projection, incremental sync, console data flow, tests, and operational risks.\n\nAI-assisted-by: gpt-5.6-luna --------- Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro> Co-authored-by: Alejandro Alonso <alejandroalonsofernandez@gmail.com> Co-authored-by: Andrey Antukh <niwi@niwi.nz> |
||
|
|
582187dd03
|
🎉 Allow to validate and repair files from /dbg (#11335) | ||
|
|
3033da4409
|
🐛 Add concurrency limit to import-binfile RPC handler (#11024)
Apply climit with 4 global permits and 1 per-profile permit (queue 2) to prevent connection pool exhaustion from concurrent imports. Each import holds a DB connection for its entire duration with idle transaction timeout disabled, so unbounded concurrency could exhaust the pool (default 60 connections). AI-assisted-by: mimo-v2.5-pro |
||
|
|
a131e40a6d |
✨ Add proper rlimit config and propagate limit timestamp
Replace the placeholder rlimit.edn with a real per-endpoint configuration covering auth, SSRF, search, email, media and project operations. The previous file only had a commented-out example, so all limits fell back to the 200k/h default window. Also propagate the evaluated `now` timestamp into both bucket and window result maps, so consumers (e.g. soft-mode reports) can know exactly when the limit was checked. AI-assisted-by: minimax-m3 |
||
|
|
4f7bb94bb1
|
🐛 Add size limit and rate limiting to send-user-feedback (#10979) (#10990)
Prevent email bombing attacks on the send-user-feedback endpoint by limiting the error-report field to 1MiB and adding climit rate limits: by-profile (1 permit, queue 3) and global (4 permits), configured in climit.edn. Make the schema public so it can be exercised by tests, and add schema validation tests covering the new size limit. AI-assisted-by: qwen3.7-plus |
||
|
|
94f51afb20
|
♻️ Normalize organization naming across Penpot (#10977) | ||
|
|
056cd3d379 | 🐛 Fix SSO review | ||
|
|
c467d98c9e | Merge remote-tracking branch 'origin/staging' into develop | ||
|
|
2344ba22a6 |
🎉 Add error reports API and CLI tool
Implement RPC methods for querying server error reports with pagination and filtering. Add CLI tool (tools/error-reports.mjs) for convenient access with table and JSON output formats. Extract profile-id from audit events and logging context for better error categorization. Build improved HREF using request path when available. AI-assisted-by: qwen3.7-plus |
||
|
|
27392abd49 | 🐛 Increase team name abbreviation limit in invitation emails | ||
|
|
2a5b6a69ad
|
✨ Send warning for email about nitrate orgs with sso (#10413) | ||
|
|
4a41b2e5e0
|
♻️ Update in-app onboarding slides (#10086)
* ♻️ Modify social media icons in verification email * ♻️ Update verify email * ♻️ Update copies in 'check your email' * ♻️ Update onboarding images * ♻️ Refurbish create team slide * ♻️ Refactor SCSS for in-app onboarding * 🐛 Fix replace old uxbox with penpot image for all email HTMLs * 🐛 Fix use of link component |
||
|
|
93b9f5c567
|
🐛 Follow 302 redirects when downloading templates (#10308)
* 💄 Update URIs for templates to avoid redirects * 🐛 Follow 302 redirects when downloading templates |
||
|
|
785b07313b | ✨ Add nitrate endpoint to send renewal email | ||
|
|
3858993a57 | Merge remote-tracking branch 'origin/staging' into develop | ||
|
|
f6c76711f4 | Merge remote-tracking branch 'origin/main' into staging | ||
|
|
1d8da08144
|
🐛 Fix typo in organization name invitation email (#9817) | ||
|
|
405a73e8ba
|
✨ Add climit impl and config for file snapshot methods (#9722)
* ✨ Add dedicated concurrency limit for restore-file-snapshot This adds a dedicated climit configuration for the restore-file-snapshot RPC method with :permits 1 per profile (plus queue of 2 and 60s timeout) and a global limit of 3. Previously the method only used the generic root/by-profile and root/global limits, allowing up to 7 concurrent restore operations per profile which caused database row lock contention on FOR UPDATE and connection pool exhaustion. * ✨ Skip locking on restore! to avoid blocking other operations Changes the row lock acquisition in restore! from a blocking FOR UPDATE to FOR UPDATE SKIP LOCKED. If the file row is already locked by another concurrent operation (e.g., another restore or an update-file), the query returns no rows and the caller fails fast with a clear conflict error instead of blocking indefinitely holding a database connection. * ✨ Add queue and timeout limits to root/by-profile concurrency limit Previously root/by-profile had no queue limit (unbounded Integer/MAX_VALUE) and no timeout, allowing requests to pile up indefinitely behind a profile whose permits were exhausted by long-running operations. This could lead to memory pressure and cascading failures. Now limited to 30 queued requests with a 30-second timeout so excess requests fail fast. * ✨ Move backup snapshot creation outside restore transaction The backup snapshot (fsnap/create!) is now created in its own short-lived connection before the actual restore transaction begins. This ensures the backup is persisted independently of the restore outcome and reduces the restore transaction window. The restore itself runs inside a db/tx-run! block with an optimistic locking check: it reads the file with FOR UPDATE and compares its revn against the value captured at backup time. If the file was edited concurrently, the restore aborts with a conflict error to prevent data loss. Co-dependent with the SKIP LOCKED change in restore! — the FOR UPDATE acquired here is in the same transaction as restore!, so the SKIP LOCKED inside restore! correctly sees the row as unlocked (same transaction). * ♻️ Remove unused private function get-minimal-file The local get-minimal-file function in file_snapshots.clj is no longer used since restore! switched to direct exec-one! with FOR UPDATE SKIP LOCKED. The sql:get-minimal-file SQL constant is still used directly. * ✨ Add minor improvements on db connection management * ♻️ Refactor create-file-snapshot to use explicit transaction management Remove automatic transaction wrapping (`::db/transaction true`) and pass `cfg` through the call chain instead of destructured `conn`. Wrap `fsnap/create!` in an explicit `db/tx-run!` for clearer transaction boundaries. Signed-off-by: Andrey Antukh <niwi@niwi.nz> * ✨ Add dedicated concurrency limit for create-file-snapshot This adds a dedicated climit configuration for the create-file-snapshot RPC method with :permits 1 per profile (plus queue of 2 and 60s timeout) and a global limit of 3. Previously the method only used the generic root/by-profile and root/global limits, allowing up to 10 concurrent snapshot creation operations per profile which could cause database contention and connection pool exhaustion. Signed-off-by: Andrey Antukh <niwi@niwi.nz> --------- Signed-off-by: Andrey Antukh <niwi@niwi.nz> |
||
|
|
c6bea65a48 | ✨ Add organization logo to nitrate invitations emails | ||
|
|
ad974f4047 | 💄 Unify naming on nitrate-api | ||
|
|
ac472c615a
|
🐛 Fix nitrate invitations org ux review | ||
|
|
5c761125f3 | ✨ Add invite-to-org to Nitrate API |