23526 Commits

Author SHA1 Message Date
Andrés Moya
45b8320ac7
🐛 Fix error when reset overrides (#11604)
* 🐛 Fix error when reset overrides on a swapped copy

* 🐛 Add regression test for reset overrides inside group

Cover the case where a nested copy lives inside a group (not directly
under the instance root). After a swap, reset overrides must undo the
swap without error (#11656).

---------

Co-authored-by: Alejandro Alonso <alejandroalonsofernandez@gmail.com>
2026-09-23 14:47:10 +02:00
David Barragán Merino
8302a984a2 🐳 Bump penpotapp images to 2.18 in docker-compose
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-23 12:32:24 +02:00
David Barragán Merino
6d4060e7a9 🔧 Release admin-console images on final tags 2026-09-23 11:17:42 +02:00
Andrey Antukh
5baffdc213 📎 Update changelog 2.18.0 2026-09-23 10:41:57 +02:00
María Valderrama
3a5481c577
✨ Error page for instances without admin-console configuration (#11812)
* ✨ Error page for instances without admin-console configuration (#11649)

* ✨ Error page for instances without admin-console configuration

* 📎 Code review

* 🐛 Fix link nitrate-not-configured
2026-09-23 10:36:40 +02:00
Andrey Antukh
f56d4c56d9 Merge remote-tracking branch 'origin/main' into staging 2026-09-23 10:33:24 +02:00
David Barragán Merino
40ef4a90d5 📚 Document Admin Console setup for Docker (>= 2.18)
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-22 20:00:54 +02:00
David Barragán Merino
d842b835d3 📚 Reorder menu entries for setup methods
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-22 20:00:54 +02:00
David Barragán Merino
c26408a568 🐳 Deploy the admin-console service in the Docker Compose example 2026-09-22 19:58:15 +02:00
Andrey Antukh
70890bb900
🐛 Store audit initiator as plain string for shared-key callers (#11629)
Shared-key callers (exporter, admin-console) arrive as keywords on 
auth-key-id, so transit persisted them as ~:exporter while regular traffic
stored app. Coerce with d/name at the single origin so every audit and
telemetry copy carries a plain string. Adds regression tests for the origin
 and the push-audit-events path, including caller spoofing precedence.

Closes #11628 

AI-assisted-by: muse-spark-1.3-contributor
2026-09-22 19:44:44 +02:00
David Barragán Merino
ec62799793 ⬆️ Align mcp runtime image to the same Node version
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-22 19:20:24 +02:00
David Barragán Merino
d8d345d8bd 🐛 Install gzip for tar -xzf in mcp's pnpm install
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-22 18:31:09 +02:00
bameda
0622680691 ♻️ Make sha mean the full commit SHA, short_sha the 12-char one
sha is the full 40-char SHA in build-bundle.yml's output and
build-docker.yml's new checkout-pinning input, matching git and
GitHub's own convention. short_sha stays internal to each workflow
for the S3 keys and image tags that already used it — build-bundle.yml
no longer exposes it externally since nothing outside consumed it.

No S3 key, image tag, or marker value changes anywhere.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-22 18:30:56 +02:00
bameda
5901af4187 🐛 Restore Dockerfile.exporter wiped by a bad conflict resolution
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-22 17:59:24 +02:00
Andrey Antukh
cc0941343d 📚 Add backend audit-log memory documentation
Add a new memory file documenting the backend audit log system:
purpose, storage schema, RPC producers, frontend ingestion,
webhooks/error-reporter/telemetry consumers, and Nexus archival.

Also wire a reference to it from the backend core memory so it
is discoverable through the memory graph.

AI-assisted-by: longcat-2.0
2026-09-22 15:49:46 +00:00
bameda
5e99e89e5b 🐛 Pin build-docker to the commit build-bundle actually bundled
_DEVELOP and _STAGING chain build-bundle.yml -> build-docker.yml,
both invoked with gh_ref as a branch name. Each reusable workflow
re-resolves that branch with its own checkout, at its own time.
build-docker's checkout happens minutes after build-bundle's, after
new commits can have landed on the branch, so it can compute a
different sha than the one build-bundle actually bundled and
uploaded to S3.

The Prepare job then requests a key that was never uploaded:

  fatal error: An error occurred (404) when calling the HeadObject
  operation: Key "penpot-sha-<newer-sha>.zip" does not exist

Expose build-bundle.yml's resolved sha as a workflow_call output and
thread it through _DEVELOP, _STAGING and _ADHOC as build-docker.yml's
new optional sha input, which pins its checkout instead of
re-resolving gh_ref. gh_ref keeps naming the branch tag to move.

_TAG is unaffected: it builds from a pushed git tag, which cannot
move the way a branch can, so build-bundle and build-docker already
agree on one commit there.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-22 16:37:06 +02:00
David Barragán Merino
0d2632fc8c 🐛 Create /usr/local/bin before extracting pnpm in DHI images
dhi.io/node:24.20.0-debian13-dev does not pre-create /usr/local/bin
the way a regular Debian image does, so tar -xzf ... -C /usr/local/bin
failed with "Cannot open: No such file or directory" right after the
checksum check passed. Introduced in #11790 when pnpm moved from
Corepack to a downloaded standalone binary.

Dockerfile.media-processor and docker/devenv/Dockerfile are unaffected:
both extract into /opt/node/bin, which already exists from the prior
Node.js install step.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-22 16:36:41 +02:00
Alonso Torres
efb10c82a8
🐛 Fix silent fail in plugin API interactions (#11781) 2026-09-22 12:56:35 +02:00
Alonso Torres
31b73460c3
🐛 Fix problem with viewer url (#11803) 2026-09-22 12:56:01 +02:00
Andrey Antukh
b9bb62b497
🐛 Block MCP REPL server in multi-user mode and decouple its host (#11632)
The unauthenticated REPL server started in --multi-user mode whenever DEVENV or REPL_ENABLE was set, unlike developer tools which are already blocked there. Gate its creation behind shouldStartReplServer, mirroring the developer tools policy, so it never starts with --multi-user. Give it a dedicated PENPOT_MCP_REPL_HOST knob defaulting to localhost so it no longer inherits the main server 0.0.0.0 bind in Docker, keep the devenv script on 0.0.0.0, and document and test the new behavior. Closes #11631 AI-assisted-by: muse-spark-1.3-contributor
2026-09-22 12:17:59 +02:00
Andrey Antukh
b5274a4476
🐛 Bind MCP PluginBridge WebSocket to configured host (#11605)
The plugin WebSocket bridge ignored PENPOT_MCP_SERVER_HOST and bound all interfaces, 
exposing unauthenticated task dispatch to the network in single-user mode. Pass mcpServer.host 
into WebSocketServer, mirroring the ReplServer fix, so the bridge binds localhost by default and 
0.0.0.0 only on explicit opt-in. 

Closes #11603. 

AI-assisted-by: muse-spark-1.3-contributor
2026-09-22 11:53:23 +02:00
Andrey Antukh
5c22f5bfb7
⚡ Build the frontend bundle once for all E2E suites (#11792)
* ⚡ Build the frontend bundle once for all E2E suites

Merge tests-integration, tests-composable-suite and tests-plugin-api-suite
into one "CI: E2E" workflow. Each of the three ran its own full
frontend/scripts/build on every PR, so one PR paid the build three times.

The new build-bundle job restores actions/cache key frontend-bundle-<sha>,
runs frontend/scripts/build only on a miss and saves the key before the
job ends. The integration shards, the composable suite and the mocked
Plugin API suite now all need build-bundle and restore the same key with
fail-on-cache-miss, so none of them builds. A workflow re-run of the same
SHA reuses the cached bundle instead of rebuilding it.

Triggers become the union of the previous paths (frontend, common,
render-wasm, plugins): the bundle embeds the built plugins, so a plugins
change runs the whole set. workflow_dispatch keeps running the
integration job only, as before.

Job names are kept identical on purpose: they are the GitHub check
contexts and branch protection may match them by name.

Docs: new mem:frontend/e2e-ci-workflow records the build-once contract,
referenced from mem:frontend/core and mem:frontend/testing; the composable
memory and both suite READMEs are updated.

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

* 🐛 Fix mocked plugin suites crashing without frontend deps

The mocked CI drivers shelled out to frontend/scripts/e2e-server.js,
which imports express from frontend/node_modules. CI jobs install
only plugins/ deps, so the import failed with ERR_MODULE_NOT_FOUND
and the run timed out waiting for localhost:3000.

Serve the prebuilt bundle with a zero-dependency static server
built into each driver (ci/static-server.ts, kept in sync in both
suites) plus node:test coverage for it.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-22 10:23:15 +02:00
Andrey Antukh
d68531b783
⬆️ Update devenv dependencies (#11790)
* ⬆️ Update devenv dependencies

Update Node.js, OpenCode, clj-kondo, Babashka, Pixi, GitHub CLI, uv,
and Serena to their current stable releases.

AI-assisted-by: gpt-5.6-sol

* ⬆️ Update devenv to Java 27

Use Zulu JDK 27 in the development image for compatibility testing.
Update the official checksums for both supported architectures.

AI-assisted-by: gpt-5.6-sol

* 🐳 Replace MinIO with RustFS in devenv

Run RustFS as the development S3 service and wait for its health check.
Install a pinned AWS CLI with checksums and use it to create the bucket
idempotently from each backend entry point.

Keep the old MinIO volume untouched and use a new RustFS volume.

AI-assisted-by: gpt-5.6-sol

* 🐳 Replace MailCatcher with persistent Mailpit

Run Mailpit as the devenv SMTP sink while preserving mailer:1025 and the
localhost:1080 UI.

Store its SQLite inbox in a named volume and wait for the readiness
endpoint before starting runtime containers. Bind the web UI to loopback so
development emails stay local.

AI-assisted-by: gpt-5.6-sol

* ⬆️ Update Node.js to 24.21.0

Align the host NVM version with the Node.js version used by devenv.

AI-assisted-by: gpt-5.6-sol

* ⬆️ Update devenv to PostgreSQL 18.6

Run PostgreSQL 18 with its versioned volume layout and a TCP readiness
check that ignores the temporary initialization server.

Install the matching client, create penpot_nexus, and preserve the old
PostgreSQL 16 volume for rollback or logical migration.

AI-assisted-by: gpt-5.6-sol

* 🐳 Expose RustFS ports in devenv

Publish the RustFS S3 API and management console on localhost port 9000
and 9001.

Keep both bindings on loopback so object storage is not exposed to the local
network.

AI-assisted-by: gpt-5.6-sol

* 🐳 Install standalone pnpm in devenv

Install pnpm 12.5.0 from architecture-specific release archives and
verify their published checksums.

Remove the Corepack setup while allowing pnpm to honor the project
packageManager pins.

AI-assisted-by: gpt-5.6-sol

* 🔥 Remove corepack, use system pnpm everywhere

Corepack is gone from Node 25+, so every `corepack enable` call
fails. pnpm now ships as a system binary (devenv, CI runners and
Docker images install it directly) and auto-downloads the version
pinned in `packageManager` on mismatch.

Scripts, workflows and Dockerfiles call `pnpm` straight away; the
three deploy workflows use a single `pnpm/setup@v2` step; and the
new `scripts/sync-pnpm-version` stamps all 35 `packageManager`
fields from the system pnpm, replacing the `corepack use` sweep.

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

* 🐛 Fix exporter watch missing render-wasm build step

The exporter watch compiled CLJS requiring the generated
src/app/wasm/shared.js, which only render-wasm/build export
produces. Without it shadow-cljs failed with a cryptic missing
./shared.js dependency. Run build:wasm before watching, as
the frontend watch:app and exporter scripts/build already do.

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

* 🔧 Add opencode V2 support and adapt plugins

Register the penpot tools for both opencode V1 (server())
and V2 (setup() with JSON Schema inputs) from a single
dependency-free plugin file, sharing the psql and
paren-repair runners between both paths.

Install the opencode2 binary side-by-side with V1 in the
devenv image and document the dual registration in the
paren-repair and psql memories.

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

* ⬆️ Update pnpm and opencode
2026-09-22 10:22:31 +02:00
Andrey Antukh
e05747b546
✨ Restrict optional RPC ids to user-provided UUIDs (#11777)
* ✨ Restrict optional RPC ids to user-provided UUIDs

Add ::sm/user-provided-uuid, backed by a version and variant
aware regex that only accepts v4, v7 and v8 instances. Use it
for the optional :id of the creation RPC commands so reserved
versions such as v3 are rejected at validation time. Reads
such as get-team keep the lax ::sm/uuid. Cover the predicate
and the schema on both JVM and JS runtimes.

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

* ✨ Cover id version restriction at the RPC boundary

Add backend regression tests proving the seven creation commands
reject reserved-version ids (v3) with :params-validation and
accept v4 ids (plus v7/v8 on create-team) through the real
decode and validate path. Also drop two duplicated assertions
and document the version and variant of every fixture UUID in
user-provided-test.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-22 09:45:50 +02:00
Álvaro Tejero-Cantero
d8fddf9942
♻️ Keep agent content on standard paths, drop client shims (#11719)
* ♻️ Keep agent content on standard paths, drop client shims

Agent guidance stays where the ecosystem reads it: AGENTS.md at the
root and the skills in .agents/skills. Codex scans .agents/skills and
follows symlinks, opencode lists it among its discovery locations, and
Cursor, Zed, Warp, Cline and Amp use the same project path.

Claude Code reads neither, so it needed CLAUDE.md and .claude/skills.
Committing those two paths is not a neutral default. Both are
gitignored, git treats ignored files as expendable, and a checkout
that needs the path deletes whatever a contributor keeps there without
a warning. Neither sparse-checkout nor skip-worktree offers a way out,
because both make the next pull abort. The two links are now created
by the contributor, once per clone, and documented in
.agents/README.md.

AGENTS.local.md and .agents/local/ give every client one personal
layer that composes on top of the shared one, instead of one file per
client inside the repository.

sync-workspace seeds the two links for ws1 and above, the way it
already seeds the gitignored config.js, so the devenv workflow does
not change.

AI-assisted-by: claude-opus-5
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* ♻️ Lean on Claude Code reading AGENTS.md, drop the pointer

Claude Code 2.1.277 reads AGENTS.md through the built-in agents-md
plugin, whose default mode loads it in a project that has no
instruction file of its own. Deleting the committed CLAUDE.md is what
turns that on here, so the pointer a Claude user had to create is no
longer part of the setup, and only the .claude/skills link remains:
the plugin covers instruction files, and skills still load from that
path alone.

The instruction that replaces the pointer is a removal. A CLAUDE.md,
.claude/CLAUDE.md or CLAUDE.local.md anywhere from the root down to
the working directory hands the project back to CLAUDE.md and hides
this repository's AGENTS.md, while .claude/rules/*.md and the
person's own ~/.claude/CLAUDE.md do not count and stay usable for
personal steering. CLAUDE.local.md joins the ignore list, and a hard
rule in AGENTS.md forbids all three.

The docs name the version, say the fallback has not reached Bedrock,
Vertex or Foundry, and say the stable auto-update channel runs about
a week behind the default one, so the symlink is documented as the
stopgap for those cases and as something to drop afterwards.

sync-workspace seeds the skills link alone for the same reason.

AI-assisted-by: claude-opus-5
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 📎 Remove skills directory mention on AGENTS.md

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-22 08:43:25 +02:00
Andrey Antukh
79c98b7c2e 🐛 Fix packages/ui build process 2026-09-20 11:04:24 +02:00
David Barragán Merino
3c19a026dc 🐛 Fix docker tags not promoted for already-built commits
The bundle and docker-image build/dedup checks used different cache
keys: the bundle was cached by ref name (`penpot-<gh_ref>.zip`) while
the docker image marker was cached by commit sha
(`markers/images-sha-<sha>`). A tag built from a commit already
promoted under another ref (e.g. `develop`) would rebuild the bundle
unnecessarily, while `build-docker`'s `promote` job silently inherited
the skip from `build` and never created that ref's branch tags
(`backend:<gh_ref>`, `frontend:<gh_ref>`, ...), even though the
underlying sha-tagged images already existed.

- Key the bundle S3 object by commit sha (`penpot-sha-<sha>.zip`)
  instead of by ref name, matching the docker marker's semantics.
- Drop the S3 metadata round-trip for `bundle_version` in
  build-docker.yml; compute it locally with `git describe`, same as
  build-bundle.yml (requires fetch-depth: 0 on that checkout).
- Split `promote` into two mutually-exclusive jobs, `promote` (needs
  `build` to succeed) and `retag` (needs only `prepare`, runs when
  `prepare.outputs.exists == 'true'`), each moving the `:<gh_ref>`
  branch tags to the current sha. This replaces relying on `build`'s
  skip/success state with two explicit conditions, so the tags always
  get moved regardless of which path built the images.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-18 15:25:33 +02:00
Madalena Melo
7348620fcd
📚 Update CONTRIBUTING.md with AI policy (#11782)
Include policy for AI assisted contributions and fix "link to title format" section

Signed-off-by: Madalena Melo <madalena.melo@kaleidos.net>
2026-09-18 13:09:35 +02:00
Andrey Antukh
5b3e36489c 📚 Document int?/integer? predicate coverage in Clojure memory
Review assumed int? was 32-bit; it covers Long/Integer/Short/Byte.
Note it in mem:clojure/idioms so the mistake is not repeated.

AI-assisted-by: muse-spark-1.3-contributor
2.18.0-RC7
2026-09-17 20:23:22 +02:00
Alonso Torres
2fc2a9064a
🐛 Add download report to the error toast (#11762)
* 🐛 Add download report to the error toast

* 🐛 Restore old behavior for some cases
2026-09-17 17:05:06 +02:00
Alejandro Alonso
14c3135e21 Merge remote-tracking branch 'origin/main' into staging 2.18.0-RC6 2026-09-17 11:26:23 +02:00
Alonso Torres
420aa981b2 🐛 Fix stalled saving states (#11699) 2026-09-17 11:25:36 +02:00
Andrey Antukh
9ae967c621 📚 Add plan status lifecycle and review log
Give every plan a write-restricted Status (draft, reviewed,
done) and an append-only Review Log with UTC ISO 8601 lines.

Make-a-plan creates plans as draft and is the only flow
writing reviewed, on explicit user apply. Review-plan stays
read-only. Implement-plan closes the plan to done with the
issue URL when one exists, in the same commit as the code.
Document the lifecycle in the agents README.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-16 18:12:12 +00:00
Andrey Antukh
ebba70ad2b 📚 Normalize plan followup and sub-plan naming
Define derived plan naming for .agents/plans/.

Parent basename stays intact and derivatives append
--review-NN for review followups and --task-NN for
roadmap sub-plans, with no new date so ls groups them.
Document the rule in the planner skill, the in-place
vs new-file policy in make-a-plan, and examples in
the agents README.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-16 18:00:55 +00:00
Andrey Antukh
30e52af22e 📎 Backport creating-issue serena memories from develop 2026-09-16 19:48:50 +02:00
David Barragán Merino
443622b2f9 🐳 Keep dist-upgrade unattended across the image builds
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-16 18:28:48 +02:00
Andrey Antukh
8ff99f0766 📚 Document how to add issues as sub-issues
Add the verified REST procedure for linking an issue as a
sub-issue of an umbrella/EPIC: get the REST id, POST to the
parent's sub_issues endpoint with a typed -F field, and verify
both directions. Route it from the create-issue skill.

AI-assisted-by: deepseek-v4.1-flash
2026-09-15 19:56:48 +02:00
Andrey Antukh
e07bda4fe2 📎 Backport .agents from develop 2026-09-15 19:56:37 +02:00
Andrey Antukh
02e573100a 📚 Update changelog 2026-09-15 19:03:39 +02:00
Chulgil Lee
cdda6ae33c 🐛 Fix form select label resetting to its default option (#11664)
select* resets its label to `default-selected` whenever its options
change identity. The access token dialog builds its options vector
inline, so picking an expiration re-rendered the dialog and the label
snapped back to "Never", although the form kept the picked value and
the token was created with it.

form-select* now passes the form value as `default-selected` when it
holds a non-blank string, so the reset lands on the picked option. The
caller's default still applies while the form value is blank.

Closes #11663

AI-assisted-by: claude-opus-5

Signed-off-by: chulgil <2044587+chulgil@users.noreply.github.com>
Co-authored-by: chulgil <2044587+chulgil@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-15 18:56:12 +02:00
David Barragán Merino
1f64cc3e52 🐛 Make integration test summary non-fatal on staging
The summary step reads a filter from .github/scripts, which never
landed on this branch, so every run failed with exit code 2 even when
all tests passed. Add the filter and tolerate both a missing file and
a jq error.

Use `!cancelled()` instead of `always()` on the merge job, so it no
longer runs after cancel-in-progress killed the shards without
uploading their blobs.

Lower the JSON report retention to 7 days, the repository maximum.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-15 18:55:11 +02:00
Andrey Antukh
39ca4c264c
🔥 Remove onboarding A/B test and welcome file creation (#11707)
Drop the onboarding-03 experiment consulted through
external-feature-flag, keeping the false-branch behavior: registration
never requests a welcome file and the workspace never shows the
onboarding modals. Remove the now-unused welcome-file machinery on the
backend (RPC wiring, welcome_file namespace, welcome-file-id prop and
the post-login redirect). Keep the external-feature-flag helper as the
seam for future experiments and note it in mem:frontend/core.

Closes #11705

AI-assisted-by: muse-spark-1.3-contributor
2026-09-15 14:14:32 +02:00
Andrey Antukh
5d5f7fcc14 🐛 Fix workspace crash on non-string thumbnail URIs
Thumbnail :uri values arrive from the server over transit, so
media-ids decode to UUID objects instead of strings. The
resolved-uri? helper added in #11563 called
clojure.string/starts-with? on them unconditionally, raising
TypeError: str.lastIndexOf is not a function and crashing the
workspace on frame render. Guard with string? so non-string
URIs fall through to resolve-media, which stringifies them.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-14 16:33:54 +02:00
Andrey Antukh
f4cf6f46f8
🐛 Use constant-time comparison for management API shared key auth (#11534)
The management API shared-key-auth middleware was using the standard = operator for key comparison, which is vulnerable to timing attacks. The RPC middleware already uses constant-time comparison via MessageDigest/isEqual.

This change:
- Makes constant-time-eq? public in app.http.middleware
- Updates app.http.management/shared-key-auth to use mw/constant-time-eq?
- Fixes an inconsistency where the nil-key branch returned a 2-arg function
- Adds comprehensive tests for the management shared-key-auth middleware

Closes #11426

AI-assisted-by: qwen3.7-plus
2026-09-14 13:26:16 +02:00
Andrey Antukh
5931f60d53
🐛 Fix workspace crash when resolving thumbnail data URIs (#11563)
The workspace-thumbnail-by-id ref unconditionally called resolve-media
on thumbnail URIs, which caused a stack overflow when the URI was a
data URI (which can be megabytes long for large images).

Data URIs contain thousands of '/' characters (base64 uses '/' as one
of its 64 characters), causing lambdaisland.uri/join to iterate
thousands of times in remove-dot-segments and overflow the JavaScript
call stack.

Add a resolved-uri? helper that checks if the URI already starts with
'blob:' or 'data:', and skip resolve-media for those cases. Only call
resolve-media when the URI is a plain UUID (media-id from the server).

Closes #11562

AI-assisted-by: qwen3.7-plus
2026-09-14 13:25:31 +02:00
Andrey Antukh
8128e350c5
✨ Add expires-in TTL option to demo profile creation (#11574)
* ✨ Add expires-in option to create-demo-profile

Allow passing an optional expires-in duration when creating a demo profile so its purge is scheduled sooner than the global deletion delay. Values below 5 minutes or above the global delay are rejected with an invalid-expires-in validation error, resolved before any profile is created. 

Closes #11573 

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

* 🐛 Make duration schema decoding total instead of throwing

parse-duration returned by the duration schema decoder threw DateTimeParseException on invalid strings, escaping params validation as a raw error. It now returns the input unchanged so invalid values fail the duration predicate with a clean params-validation error. Closes #11573 AI-assisted-by: muse-spark-1.3-contributor

* 📎 Fix doc version for expires-in change entry

The expires-in change entry was documented under 2.20 but the current version is 2.18. 

AI-assisted-by: muse-spark-1.3-contributor
2026-09-14 13:24:38 +02:00
Andrey Antukh
8629dc6b2c
🔥 Remove unused comment profiles code in the frontend (#11624)
* 🔥 Remove unused comment profiles fan-out in dashboard

The dashboard event retrieve-unread-comment-threads issued one
get-profiles-for-file-comments call per distinct file id and stored
the result in :current-team-comments-users, a state key no one
reads. The dashboard renders each thread author from the thread
payload via get-owner, so the fan-out was N+1 dead work.

Drop the per-file branch and the state write; the event now issues
a single get-unread-comment-threads call.

AI-assisted-by: deepseek-flash

* 🔥 Remove unused file comments users event and state

fetch-file-comments-users had no callers and passed a :team-id
where get-profiles-for-file-comments requires :file-id. Its only
effect was writing :file-comments-users, a viewer state key no
one reads.

Remove the event and the unused state key. The viewer still
loads comment profiles through fetch-profiles.

AI-assisted-by: deepseek-flash
2026-09-14 13:23:44 +02:00
Elenzakaleidos
8ae188ad0b
💄 Update README.md (#11665)
Updated the description of Penpot Enterprise and added an image.

Signed-off-by: Elenzakaleidos <elena.scilinguo@kaleidos.net>
2026-09-14 12:12:29 +02:00
Andrey Antukh
e5f375edbc ✨ Disable newsletter telemetry fallback on official hosts
Skip the limited newsletter report when the public-uri host
belongs to penpot.dev or penpot.app, so the SaaS never sends
subscriber emails to its own telemetry endpoint.

Defer the subscriptions query with delay so it only runs when
a report is actually going to be sent.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-14 07:29:34 +00:00
Andrey Antukh
37f7ba4833 📚 Note general subagent delegation in flows intro
Delegating plan and review to a subagent (engineer-* or the
builtin general) starts a clean context instead of growing the
main session. Delegating to general keeps the same model.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-11 15:44:15 +00:00