Commit Graph

13862 Commits

Author SHA1 Message Date
Paul Rastoin dee653dfa6 fix: move message list member backfill command to 2.25 (#23271)
## What

Moves the `backfill-message-list-members-junction-target` workspace
upgrade command from `2.24.0` to `2.25.0`.

Introduced in #23176 (commit 59eead23), the command was registered under
version `2.24.0`. Since `TWENTY_CURRENT_VERSION` is now `2.25.0`,
`2.24.0` is an already-released version, so its upgrade commands do not
re-run and the backfill would never execute on upgrade.

## Changes

- Moved the command and its module from `upgrade-version-command/2-24/`
to a new `2-25/` directory.
- Updated the decorator from `@RegisteredWorkspaceCommand('2.24.0',
...)` to `('2.25.0', ...)`.
- Renamed `V2_24_UpgradeVersionCommandModule` to
`V2_25_UpgradeVersionCommandModule` and updated its registration in
`workspace-command-provider.module.ts`. The `2-24` module only ever
provided this single command.

The timestamp (`1784567000000`) and command logic are unchanged.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01549NK3QrcEgFsdi8UgXk1u)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23271?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 13:50:26 +00:00
Paul Rastoin ba5cb6ba15 fix(server): repair missing keyValuePair.applicationId on 2.23 upgrades (#23272)
Fixes #23254

## Problem

Upgrading a self-hosted instance from `2.23.x` to `2.24.0` leaves
`core.keyValuePair` without the `applicationId` column. Database-backed
config loading then fails on startup and on every refresh (~every 15s)
with:

```
column KeyValuePairEntity.applicationId does not exist
```

The frontend shows "Unable to reach the backend".

## Root cause

`AddApplicationIdToKeyValuePairFastInstanceCommand` was added in #23089
(after `2.23.x` shipped) but registered under the already-released
`2.23.0` segment:

```ts
@RegisteredInstanceCommand('2.23.0', 1784659343818)
```

The upgrade cursor is **positional and forward-only**:

- `resolveStartCursor` resumes at `lastAttemptedIndex + 1`. A
fully-upgraded `2.23.x` instance has its cursor at the last `2.23`
workspace command, which sits *after* this newly-inserted fast command
in the sequence. So the runner steps right over it and the DDL never
runs.
- The upgrade-aware metadata layer decides "applied" the same way
(`stepIndex < currentCursor` in
`upgrade-aware-entity-metadata.adapter.ts`). Since the step index is
below the cursor, the column is considered applied and is **not** hidden
from TypeORM SELECTs, so every query references a column that was never
created.

Fresh `2.24.0` installs replay the whole sequence, so only `2.23.x ->
2.24.0` upgrades are affected. The instance log `1 fast instance ... for
2.24.0` confirms the command landed in the `2.23.0` bundle rather than
`2.24.0`.

## Fix

- Add `RepairKeyValuePairApplicationIdFastInstanceCommand` under the
current version (`2.24.0`) with a fresh timestamp, so it sorts last in
the sequence and runs for every existing instance regardless of cursor
position. Its DDL mirrors the original command and is fully idempotent
(`ADD COLUMN IF NOT EXISTS`, `DROP INDEX IF EXISTS` + recreate, `ADD
VALUE IF NOT EXISTS`), so it is a no-op on healthy instances. `down()`
is intentionally empty: the column lifecycle is owned by the `2.23.0`
introduction command.
- Repoint the entity's `@WasIntroducedInUpgrade` to the new command so
the column stays hidden from queries until the repair has actually run,
eliminating the error window during the migration itself.

## Notes

- `2.24.0` (`TWENTY_CURRENT_VERSION`) is the correct target: the upgrade
sequence only covers previous + current versions, so a command under
`2.25.0` (a next version) would not run. If a version bump lands before
this merges, the command should be moved to the new current version.
- Follow-up worth considering: nothing currently prevents registering a
command under a version in `TWENTY_PREVIOUS_VERSIONS`. A startup
validation rejecting that would have caught this at PR time.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23272?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 13:46:59 +00:00
Weiko abe4d7491c Cap nested relation query concurrency (#23252)
## Context

Common API queries load selected relations after fetching the root
records.

Relation loading is batched: one query pipeline loads a relation for all
parent records, so this is not an N+1 problem. However, every sibling
relation currently starts concurrently through `Promise.all`.

Nested relations repeat the same behavior recursively. A wide selection
can therefore submit many independent relation query pipelines at once.

Existing query complexity and record limits restrict what can be
requested, but they do not limit how much database work starts
concurrently.

## What this changes

This PR adds a request-local FIFO concurrency limiter for nested
relation loading.

- At most four `findRelations` pipelines execute concurrently.
- One limiter is created for the outer relation-loading call.
- The same limiter is shared by every recursive level.
- Queued work starts as permits become available.
- Permits are released in `finally`, including when a query fails.

Conceptually:

```text
Before:
all sibling relations -> database concurrently
nested siblings       -> more database work concurrently

After:
all sibling relations -> FIFO queue -> at most 4 database pipelines
nested siblings       -> same FIFO queue and same limit
```

Note: Also addressing
https://github.com/twentyhq/twenty/pull/23251#discussion_r3644510597
2026-07-24 13:27:26 +00:00
Paul Rastoin 9390c28cb6 ci: report ci-shared-status-check on merge_group (#23276)
Follow-up to #23275. `ci-shared` was the one required check left off
that batch, so `ci-shared-status-check` still sits at "Expected -
Waiting for status to be reported" and blocks the merge queue.

Same fix as the other workflows: add a `merge_group` trigger and gate
`changed-files-check` with `if: github.event_name != 'merge_group'`. On
a queued candidate, `changed-files-check` skips, `shared-test` (gated on
`any_changed`) cascades to skipped, and the `always()`-gated
`ci-shared-status-check` job reports success in seconds. The full suite
still runs on `pull_request`.

---
_Generated by [Claude
Code](https://claude.ai/code/session_015mZozbvyvha1S6wsEVLBnF)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23276?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 15:27:07 +02:00
Paul Rastoin 29747f5d6b ci: report required status checks on merge_group so the queue isn't blocked (#23275)
Follow-up to #23216, which added the `merge_group`-triggered
`upgrade-mutation-guard`. This makes the merge queue actually usable.

## Why

The merge queue waits for every **required status check** to report a
conclusion on the `merge_group` candidate commit, and there is no
queue-only subset: it uses the branch's required status checks. Our
required `ci-*-status-check` contexts only trigger on `pull_request`, so
on a queued PR they sit at "Expected - Waiting for status to be
reported" and block the queue until the status-check timeout (60 min),
which then counts them as failed.

Only `upgrade-mutation-guard` (from #23216) triggers on `merge_group`,
so today it is the only check that reports in the queue.

## What

Add a `merge_group` trigger to each of the seven required-check
workflows and short-circuit the expensive work so the check reports
success in seconds, while the full suite keeps running on `pull_request`
to gate PRs. `upgrade-mutation-guard` stays the only check the queue
genuinely validates against `main`.

Mechanism: on `merge_group` the root jobs skip, everything downstream
cascades to `skipped`, and the `always()`-gated `*-status-check` job
runs, sees no failing needs, and succeeds. Kept as the same job in the
same workflow so the required-check context is byte-identical to the
PR-level one (a separate pass-through workflow could register a
different context and not satisfy branch protection).

Per workflow:

- **ci-front**: trigger only. It already cascades -
`changed-files-check` is `pull_request`-only and `front-sb-build` gates
on `push || any_changed`, so nothing runs on `merge_group`.
- **ci-server**: trigger + `if: github.event_name != 'merge_group'` on
the three ungated root jobs (`changed-files-check`,
`upgrade-changed-files-check`,
`server-previous-version-upgrade-mutation-guard`). The guard would
otherwise fail on `merge_group` since `pull_request.base.sha` is empty
there; the queue-side guard in `ci-merge-queue.yaml` already covers that
case.
- **ci-sdk / ci-website / ci-test-docker-compose**: trigger + the same
guard on `changed-files-check`.
- **ci-twenty-apps**: trigger + the guard on `discover` (its
`ci`/`integration` jobs gate on `discover` output, so they cascade off).

## Settings note

This complements the branch-protection changes for the queue (enable the
queue, max group size 1, per #23216). If the branch has **other**
required checks beyond these seven whose workflows are
`pull_request`-only, they need the same `merge_group` treatment or the
queue will wait on them too.

---
_Generated by [Claude
Code](https://claude.ai/code/session_015mZozbvyvha1S6wsEVLBnF)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23275?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 15:18:26 +02:00
Paul Rastoin bb4e427196 ci: run upgrade mutation guard in the merge queue against main (#23216)
Follow-up to #23215 (merged). Rebased on `main`.

## Why

#23215 fixes an instance of a class of bug: an upgrade command whose
version is chosen at `generate:instance-command` time from
`TWENTY_CURRENT_VERSION`, then left behind when `main` bumps the version
before the PR merges (base-drift). The command ships one minor early and
instances already on the newer version skip it forever.

The existing `server-previous-version-upgrade-mutation-guard` in
`ci-server.yaml` runs on `pull_request`, so it validates against the
PR's base. When the base is stale (main moved after the branch was cut),
the guard reads the branch's own `TWENTY_CURRENT_VERSION` and the check
passes even though the command is now a version behind main. That is
exactly how the original bug slipped through.

## What

The version-directory and append-only-timestamp validation is extracted
into a shared composite action,
`.github/actions/upgrade-mutation-guard`, diffed against a
caller-supplied `base_sha`. It is called from two places:

- **`ci-server.yaml`** (PR-level guard, `base = pull_request.base.sha`)
for fast feedback. The job keeps its existing name/check. This replaces
~290 lines of inline shell.
- **`ci-merge-queue.yaml`** (new, `merge_group`-triggered, `base =
merge_group.base_sha`). GitHub builds each merge-queue candidate on top
of the current tip of `main`, so the checks read
`TWENTY_CURRENT_VERSION` and the existing per-directory timestamps from
main's real state at merge time.

Because the candidate is rebased onto main, base-drift is caught by
construction: the same validation simply runs where the base is
guaranteed current. No origin/main comparison hack; the logic now lives
in one place.

## Bypass semantics

The guard has two independent checks, and they are treated differently
on purpose:

- **Version-directory check** keeps its
`ci:allow-previous-version-upgrade-mutation` bypass, a deliberate,
reviewed escape hatch for legitimately touching a previous-version
directory. The PR-level guard reads the label directly; the merge-queue
guard resolves it from the queued PR (the `merge_group` event carries no
labels) and passes it to the composite action, which skips only the
version-directory step.
- **Timestamp / append-only check has no bypass.** The old
`ci:allow-upgrade-command-timestamp-exception` label is removed. A fake
or out-of-order timestamp rewinds the upgrade cursor and re-hides
already-applied columns, so there is no "allowed" version of it: the
timestamp just has to be configured correctly (real epoch millis,
strictly greater than every existing command in the same version
directory). If a blocking existing max is itself a fabricated future
timestamp, re-slot that command to its real merge epoch rather than
reaching for a bypass.

Preventing previous-version mutation is the guard's primary purpose. In
the merge queue the guard job always runs and skips only the
version-directory step when the bypass label is set, so it reports a
real success/failure (the required check never resolves to a skipped
state, and a label-lookup failure fails closed) and the timestamp check
always runs.

## Requires a settings change (not in this diff)

Enabling the merge queue and marking the check required are
branch-protection settings, not file changes. After merge, an admin
needs to:

1. Enable the merge queue for `main` in branch protection.
2. Add `CI - Merge Queue / upgrade-mutation-guard` to the merge queue's
required checks.

## Notes

- Composite action, not a `workflow_call` reusable workflow,
deliberately: converting the `ci-server.yaml` job to a reusable-workflow
call would rename its status check to
`server-previous-version-upgrade-mutation-guard / ` and break that
required-check mapping in branch protection. A composite action dedups
the logic while keeping both callers' check names intact.

---------

Co-authored-by: Paul Rastoin <paul.rastoin@gmail.com>
2026-07-24 14:37:45 +02:00
github-actions[bot] c0c5ba33e8 i18n - translations (#23269)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23269?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 14:20:10 +02:00
github-actions[bot] b61134ea5b i18n - translations (#23268)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 14:14:44 +02:00
Raphaël Bosi 923035bf48 Extract front component host wrapper into hooks (#23262)
Refactors `createHtmlHostWrapper` into composable hooks
(`useHtmlHostElementProps`, `useComposedElementRef`,
`useCaretPreservingElementRef`) as groundwork for the geometry mirror.

Behavior-focused, no feature change:
- Caret preservation moves to a stable ref + `useLayoutEffect`
re-assertion (covered by the caret suites). Highest regression surface
in the series, isolated here for focused review.
- The remote `ref` prop is now swallowed via `INTERNAL_PROPS` instead of
leaking onto host elements.

First of three PRs splitting the geometry mirror work.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23262?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 14:09:40 +02:00
Raphaël Bosi 3ee8fc0973 Add front component skeleton loader (#23261)
Front components (dashboard widget, side panel, settings preview) showed
blank space during their entire load. They now show a shimmering
full-area skeleton continuously, from the lazy chunk load through
metadata fetch, token/SDK wait, and worker boot, until the real UI
mounts.

The skeleton is threaded down as an optional `loadingFallback` prop so
the shared `twenty-front-component-renderer` package stays
dependency-free (react-loading-skeleton stays in twenty-front). The
command-menu headless component opts out by not passing a fallback, so
it stays blank as before.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23261?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 14:09:16 +02:00
Félix Malfait 3a8f086d15 Converge drag and drop on shared dnd-kit primitives, remove @hello-pangea/dnd (#23211)
Follow-ups recorded in #23023, done in one pass.

## Shared primitives

- Folded `PageLayoutWidgetSortableItem` and `PageLayoutWidgetDropLine`
into the shared `DragDropItemSortableCell` / new `DragDropItemDropLine`
(new `data`, `dropLine`, `highlightWhileDragging`, `hasTransition`
props).
- Added generic `DragDropProviderDragStartEvent` (and
DragMove/DragOver/DragEnd/DropTarget) helpers and deleted the 7 copied
`Parameters<...>` extractions across the dnd hooks.
- Replaced the `useMovePageLayoutWidgetUp/Down` implementations (~140
lines) with `moveWidgetWithinTabInDraft`.
- Migrated the remaining page-layout test suites onto
`pageLayoutDraftFixtures`.

## Tab reordering off Pangea

- Tabs are sortable cells on the same provider as widget drags,
segregated by dnd type, so widget drops on tab buttons keep working
while tabs reorder.
- Reordering is ID based (`reorderTabInDraft`: insert before the hovered
tab), which keeps the pinned first tab in place without index
arithmetic.
- Preserved overflow behaviors: the dropdown stays open while a tab drag
is in flight, dropping a tab on the "+N More" button appends it and
opens the dropdown, and both the visible strip and the overflow list
have end drop zones.

## Fields configuration editors off Pangea

- Group reorder, field reorder and cross-group field moves now run on
the shared cells (same drop line and end-zone patterns).

## DraggableList off Pangea

- `DraggableList` / `DraggableItem` keep their consumer-facing API — the
~9 consumers now type their handlers with a local
`DraggableListDropResult` instead of pangea's `DropResult` — but run on
the shared sortable cells; each list's uuid group doubles as its dnd
type so nested lists stay isolated from page-level providers.
- Items register their index in a list-scoped registry so the end drop
zone can resolve the append index at drop time (with insert-before
semantics an item could otherwise never reach the last position).
- Deleted three dead files that only existed for pangea plumbing (the
side panel navigation placeholder, `getCssCompatibleDraggableProps`, the
orphaned `recordGroupPendingDragEndReorderState`).

## Record table row drag off Pangea

- Rows register through `useSortable` directly on the row element — no
wrapper div, so row CSS, sticky cells and virtualization stay untouched
— with the grip cell wired as the drag handle via the shared sortable
handle ref context.
- Both table modes (virtualized flat list and record groups) share a
`DragOverlay` clone that replaces pangea's virtual-mode `renderClone`,
and end drop zones per record group (and after the virtualized list)
allow dropping after the last row or into an empty group.
- The drop handlers keep their pangea-shaped result object, retyped as a
local `RecordDragDropResult`, so the position computation logic is
untouched.

## Pangea removed

`@hello-pangea/dnd` is gone from `package.json` and the lockfile, along
with its orphaned transitive entries (`css-box-model`, `raf-schd`,
`react-redux`, `redux`). Nothing in the repo imports it anymore.

## Dashboards: cross-tab widget drag for grids

react-grid-layout drags never enter dnd-kit, so the bridge hit-tests the
pointer against the tab buttons' `data-page-layout-tab-drop-target-id`
rects during grid drags, highlights the hovered tab through state, and
on drop moves the widget to the destination grid below its existing
content (`moveWidgetToGridTabInDraft`, `buildTabWidgetLayouts`). The
grid's own post-drag layout commit is suppressed once so it does not
overwrite the cross-tab move.

## Fixes found while testing

- With `feedback: 'clone'`, the drag source is its own initial drop
target and its placeholder is a DOM clone taken at drag start, so the
drop line rendered into the source got baked into the placeholder and
stuck there for the whole drag. The line is now hidden on the source
cell, leaving a single indicator at the actual target.
- Reorderable tabs collapsed to text height and sat top-aligned next to
"+ New Tab" because the sortable cell wrapper defaults to `display:
block; height: auto`, breaking the tab height chain — the tab list now
uses the cell's `fill` mode so tabs stretch to the strip height again.

## Testing

Playwright against the dev app:
- Record page: widget reorder up and down in the pinned column (single
blue drop line at the target), drag to another tab via its tab button
(highlight + move), drag back into content at a specific position,
chained cross-tab moves, tab reorder with vertical drop line, new tab
creation.
- Overflow (narrow viewport): drop a tab on "+N More" (appends last,
dropdown opens), reorder inside the dropdown (stays open), drag a tab
from the dropdown back to the visible strip.
- Dashboard: grid drag within a tab, cross-tab drag onto a tab button
(hover highlight, widget lands below destination content, remaining
widgets keep their positions), save and reload persistence in both
directions.
- Fields editor: field reorder, group reorder, field move across groups,
plus the Move Up / Move Down widget actions.

Since the pangea-removal commits:
- Typecheck, oxlint and oxfmt green over the full front source; unit
suites green including the migrated `useStartRecordDrag` test (jest
needed a scoped transform exemption for `@preact/signals-core` once
dnd-kit reached the side-panel suites).
- Storybook visual regression unchanged across ~700 stories — expected,
since the migrated surfaces render identical DOM at rest (drop lines and
drag overlays only exist mid-drag).
- The tab strip fix reverses the exact regression mechanism: the
sortable cell wrapper defaulted to `display: block; height: auto`,
collapsing the tab height chain next to the full-height "+ New Tab"
button; `fill` restores the stretch.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XKRCzzu8oGyocXZtFp7VEG)_


<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23211?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``&lt;img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"&gt;``</a>
2026-07-24 14:04:13 +02:00
Abdullah. d53ef11fe2 fix: bump linkify-it to 5.0.2 (Dependabot) (#23236)
Bumps **linkify-it -> 5.0.2** (sole descriptor `^5.0.1`, caret already
permits it; recursive `yarn up`, lockfile-only, no resolution). Clears
[1799](https://github.com/twentyhq/twenty/security/dependabot/1799)
(GHSA-v245-v573-v5vm, high). `yarn install --immutable` passes. 5.0.2
published 2026-07-01, clears the age gate.
2026-07-24 16:38:58 +05:00
Raphaël Bosi bb22b216db Fix front components rendering a blank panel in Firefox (#23213)
Fixes #22973

In Firefox, front components rendered a blank panel with only
`DataCloneError: Exception object could not be cloned` in the console.
Accessing `caches` in the opaque-origin sandbox worker throws a Gecko
`Exception` (worker-side CacheStorage code up to v2.22, or any component
code touching it since), and `@quilted/threads` posts thrown values raw
over the MessagePort. Firefox cannot structured-clone these exceptions,
so the error report itself failed and the render promise never settled.

Thread errors are now flattened to clonable payloads and rehydrated on
the other side, so the real error surfaces in the error box instead of
silently hanging the panel. Also makes the CacheStorage guards
exception-safe (v2.23 already moved that code host-side, which removed
the main trigger).

Verified end to end in stock Firefox 149: before, a component touching
`caches` hangs silently; after, render rejects with the full
`NS_ERROR_FAILURE` diagnostic. Chromium behavior unchanged.

```mermaid
sequenceDiagram
    participant Host as Host (React)
    participant Worker as Sandbox worker (null origin)
    participant Threads as @quilted/threads

    rect rgb(250, 235, 235)
    note over Host,Threads: Before — Firefox hangs
    Host->>Worker: render(component)
    Worker->>Worker: throws Gecko Exception<br/>(typeof caches)
    Worker->>Threads: postMessage(rawException)
    Threads--xHost: DataCloneError:<br/>Exception could not be cloned
    note over Host: CALL_RESULT never arrives<br/>render() promise never settles → blank panel
    end

    rect rgb(232, 245, 233)
    note over Host,Threads: After — error surfaces
    Host->>Worker: render(component)
    Worker->>Worker: throws Gecko Exception
    Worker->>Threads: serialize → { name, message, stack }
    Threads->>Host: postMessage(clonable payload)
    Host->>Host: rehydrate → Error, reject render()
    note over Host: error box shows NS_ERROR_FAILURE
    end
```

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23213?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 13:14:47 +02:00
github-actions[bot] ae2c8978d1 i18n - docs translations (#23258)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 13:12:20 +02:00
Weiko e5c9fcf058 Add PostgreSQL connection pool pressure metrics (#23251)
## Summary
- Add pool gauges for total, idle, waiting, and maximum connections
- Record PostgreSQL connection acquisition duration and failures
- Instrument core, workspace primary, and optional replica data sources
- Add unit tests covering gauges, acquisition timing, failures, and
deduplication


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23251?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 13:03:12 +02:00
github-actions[bot] d0863dd1f7 i18n - translations (#23253)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 12:10:12 +02:00
Félix Malfait 1fdb5605f1 feat: kanban, calendar and grouped-table layouts for relation field widgets (#23112)
## Context

A relation field widget on a record page can already embed a
record-scoped view rendered as a **table** (`FieldDisplayMode.TABLE`) —
e.g. a Company's Opportunities. This brings **kanban, calendar and
grouped-table** to that same embedded view, so the board/calendar stays
scoped to *this* record's related records (not a standalone all-records
widget — that was the earlier #23003 approach, closed).

Builds directly on the merged dashboard widget layouts (#22963), reusing
its renderer, draft/save pipeline, and settings dropdowns.

## Approach — extend the existing "Table" display mode

The relation field widget already stores a `viewId` and renders it
through the layout-agnostic `RecordTableWidgetRendererContent` (which
branches on the embedded view's `type`), scoped to the current record
via `RecordFilterValueDependenciesContext`. So rendering + persistence
already work for any widget view type — only the authoring UI and one
server gate were missing. **No new `FieldDisplayMode`, no data
migration.**

## Server

- `view-widget-upsert.service.ts`: a field widget in table display mode
(`isFieldTableWidget`) could already persist viewFields/filters/sorts
through this path, but was **blocked from updating view settings**
(`type` / group-by / calendar), pinning its embedded view to a table.
The widget-type guard earlier in the method already rejects every widget
kind other than record-table and field-table, so the now-redundant
record-table-only guard on the view-settings branch is dropped. The
allowed-widget-view-types check and the downstream group-by /
calendar-field validations still apply equally.

## Frontend

- **One merged Layout picker.** The field widget's Layout dropdown lists
**Field / Card / Table / Kanban / Calendar** in a single flat list — you
pick Kanban directly, instead of "Display as: Table" first and a
separate embedded-view layout second. Picking a view layout selects the
`TABLE` display mode under the hood, seeds the record-scoped embedded
view on first use (with a default group-by / date field), and applies
the layout in the same click. Kanban/Calendar are disabled with a hint
("Needs a Select field" / "Needs a Date field") when the relation target
can't support them — same gating as the dashboard picker. The row's icon
and description reflect the effective selection (e.g. Kanban), and the
dropdown mounts the draft-init effect so switching straight from
Field/Card to Kanban works before the table renderer has ever mounted.
- **Contextual rows** (Group by / Date field / Calendar view / Hide
empty groups) extracted from the dashboard panel into a reusable
`WidgetViewLayoutSettingsRows` (source object passed in — fixed to the
relation target; no Source / Limit rows) and surfaced under the picker
while a view layout is active. Its standalone layout row is hidden here
(`isLayoutRowHidden`) since layout lives in the merged picker.
- Reuses the dashboard draft snapshot + `upsertViewWidget` save pipeline
and the group-by/calendar dropdown components unchanged.

## Scope

- **One-to-many relations only** (matches the existing
`getFieldWidgetAvailableDisplayModes` gate; junction / many-to-many stay
table-only — a pre-existing inconsistency left untouched here).
- Field-widget **calendars inherit the dashboard's behavior** (month
read-only by default; day/week + drag-to-reschedule only behind
`IS_CALENDAR_WEEK_VIEW_ENABLED`), since it's literally the same
renderer.

## Tests

- Server integration
(`upsert-view-widget-view-settings.integration-spec.ts`): a FIELD +
TABLE widget can switch its embedded view to `KANBAN_WIDGET` (with
group-by) and `CALENDAR_WIDGET` (with date field), and the kanban
group-by validation still applies through the newly-opened path.
- Front unit: `getWidgetViewLayoutSettingsItemIds` (keyboard-nav row ids
per layout/flag/group state).

## Follow-ups (intentionally not in this PR)

- Migrate the dashboard settings panel onto the shared
`WidgetViewLayoutSettingsRows` (kept out to avoid churning the
just-merged #22963 file; behavior-preserving refactor).

https://claude.ai/code/session_01E5N87kwwZWhDtEQaP72cMf
2026-07-24 12:01:53 +02:00
github-actions[bot] a3a6a55051 i18n - docs translations (#23250)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 11:26:33 +02:00
martmull fdb8865933 test: cover uninstall logic function hook execution (#23249)
Follow-up to #23227 ([review
comment](https://github.com/twentyhq/twenty/pull/23227#pullrequestreview-4771629391)):
adds integration coverage for the `defineUninstallLogicFunction` hook
execution on uninstall.

## What

New integration suite
`successful-uninstall-application-logic-function-hook.integration-spec.ts`
that drives the real `syncApplication` / `uninstallApplication` GraphQL
flow and asserts on the executor wiring:

- When the synced manifest declares an `uninstallLogicFunction`,
uninstalling the application resolves the hook and calls
`LogicFunctionExecutorService.execute` exactly once, before deletion,
with the `{ version }` payload.
- When the manifest declares no uninstall hook, uninstalling the
application does not call the executor.

The executor is spied via the running app container
(`getAppProviderByClassName`) and stubbed to a success result, so the
test verifies the server-side resolution/trigger path deterministically
without depending on the local function runtime.

## Test plan

- `npx jest --config ./jest-integration.config.ts
successful-uninstall-application-logic-function-hook` passes (2/2).
- Typecheck green for twenty-server; oxlint and oxfmt clean on the new
file.


---
_Generated by [Claude
Code](https://claude.ai/code/session_016zJPggkVEw1V7SqnPSiUQx)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23249?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: martmull <martin@twenty.com>
2026-07-24 09:26:04 +00:00
martmull f793f3c5a9 Fix application logos resolving to null on install and sync (#23245)
## Problem

Application icons are no longer resolved: `logo` is `null` on
`FindManyApplications` / `FindOneApplication`, so app chips and the
settings applications table fall back to initials avatars.

## Root cause

Manifests produced by the current SDK carry the logo path in
`manifest.application.logo`; `logoUrl` is deprecated and stripped by
`normalizeApplicationAssets` (external URLs are dropped, relative ones
are moved to `logo`). Three server call sites still read only the
deprecated `logoUrl`:

- `application-sync.service.ts` (`syncApplication`): wrote `logo:
manifest.application.logoUrl ?? null` on every install/upgrade/dev sync,
overwriting `application.logo` with `null`
- `application-sync.service.ts` (`buildVirtualDryRunFlatApplication`):
same read on the dry-run path
- `application-install.service.ts` (`ensureApplicationExists`): same
read on the create path

Since both `Application.logo` and the `Application.logoUrl` resolve
field derive from that column, icons went null everywhere.

Separately, OAuth-only apps (e.g. "Twenty CLI" from dynamic client
registration) never get a logo at all: `OAuthRegisterInput` accepts
`logo_uri` but the registration controller dropped it, so
`applicationRegistration.logoUrl` also resolves to null for those.

## Fix

- Read `manifest.application.logo ?? manifest.application.logoUrl ??
null` at all three sites, matching the fallback already used by
`importLogoFile` and `fromManifestApplicationToDisplayFields`
- Persist `logo_uri` into `applicationRegistration.logo` on OAuth
dynamic client registration; `buildLogoUrl` passes absolute URLs
through, so the consent screen and app chips can resolve it

Existing rows that were already nulled will self-heal on the next app
upgrade/sync, since the sync path rewrites `logo` from the manifest.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Us7BE5yBguYTACg5H3Y85z)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23245?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 11:08:03 +02:00
github-actions[bot] f7683b7e47 i18n - translations (#23247)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 10:52:11 +02:00
github-actions[bot] 09e20eee5f i18n - translations (#23246)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23246?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 10:49:02 +02:00
martmull abc82d66b7 Consolidate per-queue worker tuning in one explicit config file (#23229)
## Context

Follow-up to the worker configuration analysis and to the worker pool
split rolled out in twentyhq/twenty-infra#805/#806. Worker tuning was
previously spread across two partial constants (`QUEUE_WORKER_OPTIONS`,
`MESSAGE_QUEUE_PRIORITY`), and most queues silently relied on implicit
BullMQ defaults.

## What this PR does

Introduces a single dedicated file to pilot worker behavior per queue:


`src/engine/core-modules/message-queue/message-queue-worker-config.constant.ts`

`MESSAGE_QUEUE_WORKER_CONFIG` declares, for **every** queue, an
explicit:
- `priority` (applied when enqueuing, lower runs first)
- `concurrency`
- `lockDuration`
- `maxStalledCount`
- `boundedShutdownDrain`

Explicitness is enforced at compile time: the record is typed
`Record<MessageQueue, { priority: number; workerOptions:
Required<MessageQueueWorkerOptions> }>`, so adding a queue without
declaring its full configuration is a type error, and no field can be
omitted.

Wiring changes:
- `message-queue.explorer.ts` passes
`MESSAGE_QUEUE_WORKER_CONFIG[queueName].workerOptions` when creating
workers
- `bullmq.driver.ts` reads the enqueue priority from the same record
- `message-queue-worker-options.constant.ts`,
`message-queue-priority.constant.ts` and
`ai-stream-lock-duration.constant.ts` are removed (the AI stream lock
duration is inlined into the one config entry that used it)

## Behavior

No behavior change — the previously implicit BullMQ defaults
(concurrency 1, lockDuration 30s, maxStalledCount 1) are now spelled out
per queue, and the existing overrides (`ai-stream-queue`: concurrency 20
/ 10 min lock / no stall retry / bounded shutdown drain;
`logic-function-queue`: concurrency 10) and all priorities are carried
over unchanged.

## Validation

- `npx nx typecheck twenty-server` 
- `oxlint --type-aware` + `oxfmt --check` on changed files 
- `npx jest "message-queue"` (7 tests) 

Companion infra PR: twentyhq/twenty-infra#808 moves the worker pool
topology (replicas, resources, queue filters) into a dedicated
`workers.yaml` per environment.

Session: https://claude.ai/code/session_01TL6Te48Lkys5NxyG9j2Nz6
2026-07-24 10:45:16 +02:00
martmull 91f0b77fdb Only suggest vetted apps in onboarding install step (#23222)
The onboarding "Install your first apps" step now only suggests
marketplace apps where `isVetted` is true.
2026-07-24 10:44:35 +02:00
martmull fb52635d2a Add defineUninstallLogicFunction hook for applications (#23227) 2026-07-24 10:41:22 +02:00
Abdullah. cada1ef6d7 feat(website): live community stats, drop hard-coded fallback (#23047)
## Problem

The menu's GitHub star and Discord member counts almost always render
the hard-coded snapshot (49.6K / 6.6K, frozen June 2026), not live
numbers. The render-time fetches run unauthenticated from Cloudflare
Workers, whose egress IPs are shared across tenants; GitHub's
unauthenticated quota is 60 req/hr per IP, so the call is effectively
always rate-limited. Live prod today shows the frozen 49.6K GitHub count
next to a live Discord count, confirming only GitHub is affected.

## Change

- GitHub fetch sends `Authorization: Bearer $GITHUB_STATS_TOKEN` when
the env var is set, moving it onto its own 5,000 req/hr quota. Discord
keeps the public invite endpoint, which works fine from Cloudflare's
IPs.
- The hard-coded fallback is deleted rather than refreshed.
`CommunityStats` fields are now `number | null`, resolved live ->
last-good -> null, and the menu renders an icon-only chip when a count
is genuinely unavailable. A fake number can never ship.
- Last-good values persist in the worker's existing OpenNext R2 bucket
(`NEXT_INC_CACHE_R2_BUCKET`, key `community-stats/latest.json` outside
the `incremental-cache/` prefix), so a third-party outage shows the
numbers from the previous refresh. No new infrastructure.
- Revalidation tightens from 1h to 15min (~4 GitHub calls/hour, shared
cache entry across pages).
- `MenuSocial`/`MenuDrawer` import `formatCompactCount` from its module
directly: the community barrel now re-exports server-only code, and a
client value-import would pull `getCloudflareContext` into the client
bundle.

## Rollout

- twentyhq/twenty-infra#795 passes the built-in Actions token at build
time so prerendered pages ship with a real star count from the first
request after deploy.
- One manual step: set the `GITHUB_STATS_TOKEN` secret (fine-grained
PAT, public read-only, no permissions) on the twenty-website-dev and
twenty-website-prod workers. Until it exists, behavior degrades to
today's minus the fake numbers.

## Tests

5 unit tests cover the resolution ladder: live wins, cache fills a
failed fetch, null on cold-cache failure, both-fail serves cache without
overwriting, nothing written when nothing succeeded. Verified against
the dev server: menu renders live 53.3K / 6.9K.
2026-07-24 12:44:05 +05:00
github-actions[bot] d1b556f4a8 i18n - docs translations (#23244)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23244?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 09:30:24 +02:00
Rehuz 7f1e3d3541 fix(admin): prevent fallback to 'latest' string when dockerhub tags c… (#22885)
Fixes #22849 

### What changed?
When the `AdminPanelVersionService` hits a DockerHub API error (or
filters out all valid tags), it was previously hardcoded to return
`'latest'`. This caused the frontend to display `Latest version:
latest`.

I updated the GraphQL DTO to make `latestVersion` nullable, and modified
the service fallback and unit tests to return and expect `null` instead.

The frontend (`SettingsAdminVersionDisplay`) already has logic to handle
a falsy version and gracefully display `No latest version found`, so
this backend fix entirely resolves the UX issue without touching the
frontend.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22885?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 09:25:46 +02:00
Abdullah. 0a53cc745d fix: bump brace-expansion 1.x/2.x lines (Dependabot) (#23240)
Bumps **brace-expansion 1.x -> 1.1.16** and **2.x -> 2.1.2** (caret
consumer ranges permit both; recursive `yarn up`, lockfile-only, no
resolution). Clears
[1766](https://github.com/twentyhq/twenty/security/dependabot/1766) and
[1767](https://github.com/twentyhq/twenty/security/dependabot/1767) for
GHSA-3jxr-9vmj-r5cp (high, ReDoS).

**Deliberately not covered:** the 5.x line stays at 5.0.6 because
`nx@22.7.5` pins it exact (minimatch's `^5.0.x` consumers do lift to
5.0.7, but the nx copy remains), so alert
[1765](https://github.com/twentyhq/twenty/security/dependabot/1765)
stays open. Same nx-exact-pin situation as axios; will be handled in the
resolutions batch.

`yarn install --immutable` passes. 1.1.16 and 2.1.2 published
2026-07-08, clear the age gate.
2026-07-24 12:00:10 +05:00
Abdullah. d6abb26cdb fix: bump body-parser to 1.20.6 / 2.3.0 (Dependabot) (#23239)
Bumps **body-parser** on both lines: `~1.20.3`/`~1.20.5` -> **1.20.6**
and `^2.2.1` -> **2.3.0** (all within declared ranges; recursive `yarn
up`, lockfile-only, no resolution). Clears
[1779](https://github.com/twentyhq/twenty/security/dependabot/1779) and
[1780](https://github.com/twentyhq/twenty/security/dependabot/1780)
(GHSA-v422-hmwv-36x6, low). `yarn install --immutable` passes. 1.20.6
published 2026-07-09, 2.3.0 published 2026-06-15, both clear the age
gate.
2026-07-24 11:59:52 +05:00
Abdullah. 78e0c11b34 fix: bump svgo to 3.3.4 (Dependabot) (#23238)
Bumps **svgo -> 3.3.4** (sole descriptor `^3.0.2`, caret already permits
it; recursive `yarn up`, lockfile-only, no resolution). Clears
[1802](https://github.com/twentyhq/twenty/security/dependabot/1802)
(GHSA-2p49-hgcm-8545, high). `yarn install --immutable` passes. 3.3.4
published 2026-07-11, clears the age gate.
2026-07-24 11:59:23 +05:00
Abdullah. 35e6f58d70 fix: bump dompurify to 3.4.12 (Dependabot) (#23237)
Bumps **dompurify -> 3.4.12** (sole descriptor `^3.4.11`, caret already
permits it; recursive `yarn up`, lockfile-only, no resolution). Clears
[1801](https://github.com/twentyhq/twenty/security/dependabot/1801)
(GHSA-c2j3-45gr-mqc4, low). `yarn install --immutable` passes. 3.4.12
published 2026-07-11, clears the age gate.
2026-07-24 11:59:05 +05:00
Abdullah. b2a61adee7 fix: bump immutable to 5.1.8 (Dependabot) (#23235)
Bumps **immutable -> 5.1.8** (sole descriptor `^5.1.5`, caret already
permits it; recursive `yarn up`, lockfile-only, no resolution). Clears
[1796](https://github.com/twentyhq/twenty/security/dependabot/1796)
(GHSA-v56q-mh7h-f735, high) and
[1797](https://github.com/twentyhq/twenty/security/dependabot/1797)
(GHSA-xvcm-6775-5m9r, high). `yarn install --immutable` passes. 5.1.8
published 2026-06-25, clears the age gate.
2026-07-24 11:58:45 +05:00
Abdullah. 5ee50b16db fix: bump fast-uri to 3.1.4 (Dependabot) (#23234)
Bumps **fast-uri -> 3.1.4** (sole descriptor `^3.0.1`, caret already
permits it; recursive `yarn up`, lockfile-only, no resolution). Clears
[1798](https://github.com/twentyhq/twenty/security/dependabot/1798)
(GHSA-4c8g-83qw-93j6, high) and
[1805](https://github.com/twentyhq/twenty/security/dependabot/1805)
(GHSA-v2hh-gcrm-f6hx, high). `yarn install --immutable` passes. 3.1.4
published 2026-07-19, clears the 3-day age gate.
2026-07-24 11:58:33 +05:00
Abdul Rahman 148dc6dfaa Let server route resolvers answer the caller synchronously (#23233)
## Problem

A server route resolver can only return a dispatch target (`{
workspaceId, targetLogicFunctionUniversalIdentifier, payload }`), and
`ServerRouteTriggerService` always acks `202 {queued:true}`. The target
function runs off the queue, after the response has been sent, so its
return value can never reach the caller.

That makes it impossible to integrate a provider whose webhook URL has
to be proven with a handshake on the same response. Slack's Events API
is the case that surfaced it: `url_verification` sends `{ type,
challenge }` and will not accept the Request URL unless the challenge
comes back on that POST.

## Change

A resolver may now return a `Response` (the existing
`LogicFunctionHttpResponse`) instead of a dispatch target. The route
sends it as-is via `buildRouteTriggerResponse` and enqueues nothing.

- Reuses the marker and builder that HTTP route triggers already use, so
there is no new response shape.
- Dispatch results behave exactly as before; the resolver error path is
unchanged, just hoisted out of `parseResolverResult` so it runs before
the branch.
- SDK: `ServerRouteResolverResult` becomes `ServerRouteDispatchResult |
LogicFunctionHttpResponse`.

Additive: a resolver that returns a dispatch target sees no behavior
change. Previously, returning this shape threw
`RESOLVER_INVALID_RESULT`.

## Testing

`server-route-trigger.service.spec.ts` gains a case asserting the
resolver's response is sent verbatim and nothing is enqueued. 15/15
pass.

## Context

Split out of #22984 (Slack conversational assistant), which needs this
to complete the Slack Events URL verification. That PR depends on this
one merging first.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23233?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 08:56:34 +02:00
github-actions[bot] 5bc97f8591 chore: sync AI model catalog from models.dev (#23242)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23242?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-07-24 08:47:03 +02:00
Rashad Karanouh 6b99bcea7f feat(partners): require Twenty experience on apply, drop Cal success (#23223)
## Summary
- Add a dedicated **Experience** step to the partner apply wizard
(milestones, ≥200-char narrative, proof URL) before commercials
- Stop collecting `applicationNotes`; rename Expertise chrome away from
“experience”
- Replace the post-submit Cal.com embed with a review-and-reach-out
thank-you so unqualified inbound no longer books intros automatically

**Companion PR (app):** #23224 — Partner schema, submit persistence,
triage views, Tally CSV mapper (`twenty-partners` v1.4.0). Land the app
PR with or before this one.

## Test plan
- [ ] Open apply modal: wizard order is identity → profile → expertise →
experience → commercials
- [ ] Experience step blocks continue without ≥1 milestone, narrative
≥200 chars, and a valid https URL
- [ ] Submit creates/updates Partner with the three experience fields
(with #23224 deployed)
- [ ] Success screen has no Cal embed / book-later CTA
- [ ] `npx jest --config=jest.config.mjs partner-application` passes
locally (71 tests)

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-07-24 09:40:22 +05:00
github-actions[bot] 34c2e11dcb i18n - translations (#23230)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23230?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 02:37:41 +02:00
Abdul Rahman 2c79093b74 feat: agent roleUniversalIdentifier for manifest-driven role assignment (#23206)
## Summary
- Adds optional `roleUniversalIdentifier` on `AgentManifest` /
`defineAgent` so apps can declaratively assign a role to an agent (same
config shape as `defaultRoleUniversalIdentifier`).
- Wires `agentUniversalIdentifier` as a sync many-to-one FK on
`roleTarget`, and emits a deterministic `roleTarget` from the agent
during app sync (create / update / delete).
- Enables app agents (e.g. Slack assistant) to get a role on install
without postInstall hooks or manual admin assignment.



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23206?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 05:57:40 +05:30
github-actions[bot] 8b7ac02464 i18n - translations (#23226)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-23 21:33:32 +02:00
Rashad Karanouh 18fb0946e6 v1.4.0 — Raise partner bar: Twenty experience fields + triage (#23224)
## Summary
**Version:** `twenty-partners` **v1.4.0** (minor — new Partner fields +
apply contract)

- Add Partner fields `twentyExperience`, `twentyExperienceNotes`,
`twentyExperienceProofLink` and persist them from
`submit-partner-application` (≥200-char narrative at API boundary)
- Surface Twenty experience on applications / validated / per-stage
triage views and the Partner record side panel (drop empty Introduction
from that panel)
- Add pure Tally CSV match/map helpers (ops import script stays outside
the repo) for backfilling existing partners by `partnerId`

**Companion PR (website):** #23223 — Experience step on apply +
thank-you without Cal.

## Test plan
- [ ] `yarn twenty apply -r <remote>` on a workspace — Partner gains the
three experience fields
- [ ] Website apply (with #23223) persists milestones / notes / proof
link on create and email-linked update
- [ ] Applications + Validated views show experience columns; record
side panel lists experience fields
- [ ] `yarn lint` clean; `yarn test:unit` covers schema + map-tally
helpers
- [ ] After Tally campaign: dry-run then apply CSV import via local ops
script under `~/twenty/docs/superpowers-specs/raise-bar-import/`
2026-07-23 21:26:41 +02:00
neo773 59eead238d message list member backfill (#23176)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23176?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 00:55:46 +05:30
neo773 97d2b52a71 fix(front): duplicate junction chip after Add New in relation picker (#23185)
Clicking Add New in a junction relation picker rendered the newly
created target twice until a page reload. The handler appended the
created junction to the source record's store field after awaiting the
mutation, but useCreateOneRecord's post-optimistic effect had already
attached it, so the same junction id ended up in the field array twice
(single row in DB). Removes the redundant manual append in both to-many
picker flows.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23185?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 00:55:30 +05:30
twenty-pr[bot] 6623901eb4 chore: bump version to 2.25.0 (#23221)
## Summary

- Moves current version to previous versions array
- Sets TWENTY_CURRENT_VERSION to the new version
- Updates TWENTY_NEXT_VERSIONS with the next minor version
- Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same
version

## Checklist

- [ ] Verify version constants are correct
- [ ] Verify npm package versions match

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23221?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
2026-07-23 19:50:34 +02:00
Paul Rastoin b91c2a6457 fix(server): repair missing applicationRegistration.logoFileId on upgraded instances (#23215)
## Problem

closes https://github.com/twentyhq/twenty/issues/23210

Self-hosted instances on 2.23.x fail their workspace upgrade with:

```
column ApplicationEntity__ApplicationEntity_applicationRegistration.logoFileId does not exist
at UpgradePeopleDataLabsApplicationCommand.runOnWorkspace
```

The `2-21` instance command that adds
`core."applicationRegistration"."logoFileId"` was merged ~20 minutes
after the 2.22 version bump (PR #22827, `94192a2164`), so it first
shipped in 2.22 while registered under
`@RegisteredInstanceCommand('2.21.0', ...)`.

The upgrade runner resolves its start position from the last recorded
command and only moves forward. Any instance that had already run a
2.21.x binary has its cursor past that slot, so the command is skipped
permanently and the column is never created.
`UpgradeAwareEntityMetadataAdapter` decides column visibility
positionally (`index < currentCursor`), not by whether the command
actually ran, so it keeps `logoFileId` in the SELECT list and the
instance reports "Up to date" while the column is absent.

**Affected:** instances that ran 2.21.x, then upgraded to >= 2.22.
Instances that went from <= 2.20 straight to >= 2.22 replayed the full
sequence and are fine.

`logoFileId` is populated lazily by design (NULL is a supported state),
so no backfill is added.

## Changes

**1. Idempotent DDL guard in the failing workspace command**

`2-23-workspace-command-...-upgrade-people-data-labs-application.command.ts`
now ensures the column exists at the top of `runOnWorkspace`, before the
`findOne` that crashes on affected instances. It uses the core
`DataSource` (`@InjectDataSource()`) because
`core."applicationRegistration"` is instance-global, guards with a
per-process boolean in addition to the SQL-level `IF NOT EXISTS`, and
copies the full statement list (column + unique + FK constraints)
verbatim from the 2.21 command. In dry-run it probes
`information_schema.columns` and returns instead of running the crashing
query.

**2. Fast instance command in 2.23**
New
`2-23-instance-command-fast-1784823473532-add-logo-file-id-to-application-registration.ts`,
registered at the end of the 2.23 fast segment (highest timestamp),
running the same idempotent DDL. This covers the normal 2.22 -> 2.23
path and, critically, instances with zero provisioned workspaces where
the workspace command body never executes. The shared DDL lives in
`2-23/utils/ensure-application-registration-logo-file-id-column.util.ts`
so both paths stay byte-for-byte identical. Class name follows the
`Early2_4` / `Early2_5` precedent to avoid colliding with the 2.21
command.

The fix lives entirely in 2.23: instances stuck at the failing workspace
command retry it every run, and 2.22 -> 2.24 jumps still replay the 2.23
segment.

## Ops note

Instances failing right now can be unblocked immediately by running the
same `ALTER TABLE` block by hand against their core database
(byte-for-byte what the command does). Worth including in the 2.23 patch
release note.

## Verification

- New fast instance command re-slotted last in the 2.23 fast segment
(timestamp `1784823473532` > current max `1784659343818`).
- Manual repro path: boot `twentycrm/twenty:v2.21`, seed, stop, run
`upgrade` from this branch, assert the column exists and
`upgrade:status` reports 0 failed. The default v1.22 baseline does not
reproduce it (replays from cursor 0).

---
_Generated by [Claude
Code](https://claude.ai/code/session_01YAuDR585cx7FyAKoiT32j3)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23215?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Paul Rastoin <paul.rastoin@gmail.com>
2026-07-23 17:12:32 +00:00
Raphaël Bosi 5c7915ba56 Skip install apps onboarding step for invitees (#23214)
Users joining an existing workspace via an invitation link were shown
the install apps onboarding step after skipping the email-connect step,
even though the backend never assigns them that step (and apps they
selected were silently not installed).

The frontend optimistic transition unconditionally mapped SYNC_EMAIL to
APPS_INSTALLATION. It now checks workspaceMembersCount === 1, like the
existing PROFILE_CREATION to INVITE_TEAM transition, so invitees go
straight to profile creation.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23214?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-23 16:39:19 +00:00
Thomas Trompette 5160415f40 fix(workflow): create core mirror rows during workflow prefill (#23204)
## Problem
Seeded workflows are inserted during workspace activation (and dev
seeding) via raw SQL in `prefill-workflows.util.ts`, which bypasses the
ORM entirely. The async dual-write listener that mirrors `workflow` /
`workflowVersion` into `core.workflow` / `core."workflowVersion"` never
fires for these rows, so every newly activated workspace is born with:
- no core mirror rows, and
- NULL `coreWorkflowId` / `coreWorkflowVersionId` soft-refs.

This is permanent, growing drift. The core-consistency check reports
every new workspace as 2 unlinked workflows + versions, and once trigger
dispatch reads from core these seeded workflows would silently stop
working.

## Change
Insert the `core.workflow` and `core."workflowVersion"` mirror rows and
stamp the soft-refs inside the same prefill transaction. Field mapping
mirrors the dual-write / backfill exactly:
- `core."workflowVersion".workflowId` = the workspace workflow id (what
the trigger-map cache groups by)
- `triggers` = jsonb `[trigger]`
- `applicationId` = `workspace.workspaceCustomApplicationId` (throws if
missing, same contract as the sync path)
- `core.workflow.lastPublishedVersionId` = the workspace version id
- one ACTIVE core version per workflow (satisfies the partial unique
index)

Core ids are deterministic v5 (same helper/namespace as the existing
prefill ids) so the existing `.orIgnore()` re-run guard stays idempotent
— a re-run hits the PK conflict and is skipped instead of inserting
duplicate orphan core rows.

The large diff is mostly re-indentation: the step/trigger arrays were
extracted to consts so the same objects feed both the workspace and core
inserts. The step/field UUID literals are unchanged from main.

## Verification
- `nx lint:diff-with-main twenty-server`: clean.
- Typecheck (isolated `tsc`): no errors in the changed file (`nx
typecheck twenty-server` is blocked by a pre-existing `twenty-shared`
build error on main, unrelated).
- Runtime: pending a `database:reset` + cross-schema parity query
confirming zero drift on a freshly seeded workspace.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23204?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-23 16:15:05 +00:00
martmull 4d09c400a4 Remove DATABASE_EVENT_JOBS_CHUNK_SIZE and Promise.all from logic function trigger jobs (#23205)
## What

- `LogicFunctionTriggerJob` now processes a single
`LogicFunctionTriggerJobData` payload instead of an array processed with
`Promise.all`.
- Removed `DATABASE_EVENT_JOBS_CHUNK_SIZE` and the `lodash.chunk` usage
in `CallDatabaseEventTriggerJobsJob`.
- Added `bulkAdd` to `MessageQueueService` and both drivers (BullMQ
driver uses native `queue.addBulk`, sync driver processes payloads
sequentially). `CallDatabaseEventTriggerJobsJob` uses it to enqueue all
payloads in one call.
- Updated the other producers (`ServerRouteTriggerService`,
`ApplicationInstallService`, `ConnectionProviderOauthFlowService`,
`CronTriggerCronJob`) to enqueue a single payload instead of a
one-element array, and updated the corresponding specs.

## Why

Each logic function execution now gets its own queue job, so a failing
execution only retries itself instead of re-running the whole chunk, and
job-level retry/metrics apply per execution.

---
_Generated by [Claude
Code](https://claude.ai/code/session_018VCs2kopnDiZCL41eToxQF)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23205?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-23 17:50:43 +02:00
Félix Malfait 65d399c70d feat(page-layout): drag widgets across tabs on record pages (#23023)
## What

When editing a record page layout, you can now drag a widget out of one
tab and into another. The most common case works: drag from the left
column (the pinned first tab, in full mode) into the tab you're
currently viewing.

Ways to move a widget across tabs:
- **Into the visible tab's content** — drop it into another
vertical-list tab's list to place it at a precise index. A blue line
shows exactly where it will land.
- **Onto a tab button** — drop a widget onto another tab's button to
move it to that tab; the button highlights while hovered.
- **Into an empty tab / the end of a tab** — an end-of-list drop zone
(wrapping the add-widget area) accepts the widget, so an empty tab is a
valid drop target and widgets can be appended to the end of a populated
one.

Within-tab reordering keeps working as before, now with the same blue
drop-line indicator.

## How

The record-page widget list is migrated from `@hello-pangea/dnd` to
`@dnd-kit/react` (already used elsewhere in the app, e.g.
navigation-menu-item and record-board). A single `DragDropProvider`
spans the left column, the tab bar, and the active tab content, which is
what makes cross-list drag possible — Pangea scopes each list to its own
context, so cross-tab drag wasn't expressible there.

- Widgets are dnd-kit sortables grouped by `tabId`; dropping into a
different group is a cross-tab move.
- The drop line uses `useSortable().isDropTarget` on the targeted
widget, plus an end-of-list droppable for append/empty-tab.
- Each record-page tab button is a `useDroppable` target for widgets,
opt-in per vertical-list tab so canvas/grid tabs keep their native
placement.
- The drag lifecycle lives in one router hook
(`usePageLayoutWidgetDragAndDrop`) that routes to two pure, unit-tested
draft utils (`moveWidgetWithinTabInDraft`, `moveWidgetToTabInDraft`).
The side-panel "Move to tab" action shares the same
`moveWidgetToTabInDraft` util, so drag and menu paths converge on one
mutation.
- Drop resolution reuses the shared module #23071 landed:
`getDestinationIndex` compensates same-tab downward moves for the
source-removal shift so the drop line and the landing slot agree, and
the shared `preventNativeDragStart` guard stops links/images inside
widget content from starting a native URL drag.

## Scope

Deliberately staged to the record-page widget list. Not included
(follow-ups):
- Tab reordering and the field-config editors still use
`@hello-pangea/dnd`; finishing the full page-layout removal of Pangea is
separate.
- Grid/dashboard cross-tab drag (the source there is
`react-grid-layout`, which needs a cross-system bridge).
- #23071 has merged and this branch sits on it; the remaining
convergence is a follow-up: fold
`PageLayoutWidgetSortableItem`/`PageLayoutWidgetDropLine` into the
shared `DragDropItem*` cells, export a generic drag-event-type helper to
delete the 7 copied `Parameters<...>` extractions, replace
`useMovePageLayoutWidgetUp/Down` with `moveWidgetWithinTabInDraft`, and
migrate the remaining page-layout test suites onto
`pageLayoutDraftFixtures`.

## Testing

- Unit tests for `moveWidgetToTabInDraft` and
`moveWidgetWithinTabInDraft` (incl. the non-vertical destination guard);
the three suites now share one fixture module
(`page-layout/testing/pageLayoutDraftFixtures`).
- The downward off-by-one is covered by the shared `getDestinationIndex`
unit tests from #23071.
- Full `page-layout` suite green (161 files / 1055 tests), plus
typecheck, lint, and format.
- The drag interaction itself (drop precision incl. downward same-tab
drops, line/highlight, clone feedback) still needs a manual pass in the
running app.
2026-07-23 15:06:20 +02:00
Thomas Trompette 96a2456367 feat(workflow): periodic core-consistency check for workflows, versions and triggers (#23103)
Monitoring for the soft-ref migration. The `workflow` /
`workflowVersion` dual-write into core is **best-effort** (async, not
transactional; failures only go to Sentry), so `core.workflow` /
`core.workflowVersion` can silently drift from the workspace source of
truth. This adds a periodic job that detects that drift across **all
three workflow entities** and emits it as metrics.

Supersedes the earlier inline shadow-parity approach that lived on this
branch — that only covered trigger dispatch and added a cache read +
diff to every cron tick and every DB-event batch (too much hot-path
overhead). This is broader and fully off the dispatch path.

## What
A cron (`cron:workflow:core-consistency-check`, every 3 hours, wired
into `cron:register:all`). Per run:
- **Bounded**: `SELECT DISTINCT "workspaceId" FROM core."workflow"` —
only workspaces that actually use workflows (skips the large majority).
- Per such workspace, emit a drift metric per `(entity, driftType)` —
detect-only, plus a triage log:
- **workflow** and **workflowVersion** — `unlinked` / `missingCore` /
`orphanCore` / `fieldMismatch`, via cross-schema `COUNT` aggregates
(core + workspace are the same DB, so indexed joins — no rows pulled
into JS).
- **automated triggers** — the `workflowAutomatedTrigger` table vs the
`workflowAutomatedTriggerMaps` cache: `inTableNotCache` /
`inCacheNotTable` / settings `mismatch`.
- Per-workspace failures are isolated (caught → Sentry) so one bad
workspace does not stop the sweep.

## Why it is efficient
Two central `core.*` queries + a few `COUNT` queries per
*workflow-using* workspace, on a relaxed cadence, off the dispatch path.
Shardable across ticks later if needed.

## Metrics
`workflow-core-consistency/{workflow,version,automated-trigger}/drift`
counters, attribute `driftType`. Dashboards: twentyhq/twenty-infra#800.

## Not in scope
Detect-only — no auto-heal (the existing backfill/rebuild command can
heal). No dispatch or flag changes.

## Test
- The consistency SQL (workflow/version sync counts, orphan counts,
trigger read) validated against a live workspace (it surfaced real drift
there — unlinked versions + an orphan core version). The whole
cron→service→SQL→metric pipeline is proven live: the cron is already
emitting real drift counters on a running server.
- Unit specs for the service (clean → no metric; per-entity drift per
dimension; per-workspace error isolation).
- Command boots and registers via `cron:register:all` (verified).
Typecheck + lint clean.
2026-07-23 12:30:12 +00:00