Skip to content

v0.1.0-rc.2#281

Merged
jeroenrinzema merged 11 commits into
release/v0.1.0from
main
Jun 29, 2026
Merged

v0.1.0-rc.2#281
jeroenrinzema merged 11 commits into
release/v0.1.0from
main

Conversation

@jeroenrinzema

Copy link
Copy Markdown
Contributor

This pull request introduces several improvements and fixes across the codebase, focusing on three main areas: API enhancements for scheduled assignments, improved handling of entrance step data keys in the journey editor, and UI/UX refinements. The most significant changes are summarized below.


API and Type Improvements for Scheduled Assignments

  • Added optional id fields to scheduled assignment request and response schemas for both users and organizations, allowing updates and deletes by assignment instance, and clarified behavior in OpenAPI specs and generated Go types (internal/http/controllers/v1/client/oapi/resources.yml, console/src/oapi/management.generated.ts, internal/http/controllers/v1/client/oapi/resources_gen.go). [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]

Journey Editor: Entrance Data Key Auto-Assignment

  • Introduced the defaultEntranceDataKey utility and logic to automatically assign unique, readable data_key values to entrance steps, ensuring their trigger event data is referenceable downstream and backfilling legacy entrances without a key (console/src/views/journey/editor/JourneyEditor.utils.ts, console/src/views/journey/hooks/useJourneyEditorGraph.ts, console/src/views/journey/hooks/useJourneyFlowHandlers.ts). [1] [2] [3] [4] [5] [6] [7]
  • Added comprehensive unit tests for entrance data key defaulting and backfill behavior (console/src/views/journey/editor/JourneyEditor.utils.test.ts).

UI and UX Refinements

  • Improved email preview rendering for small thumbnails by showing only the rendered email, omitting the Gmail-style header chrome for better appearance in compact views (console/src/components/preview.tsx).
  • Updated campaign form logic to trigger validation when setting or clearing the subscription ID, ensuring form state is always consistent (console/src/views/campaign/NewCampaign.tsx). [1] [2]
  • Reduced the client list fetch limit in settings from 200 to 100 to improve performance or UI handling (console/src/views/settings/clients/store.ts).

These changes collectively enhance API flexibility for scheduled resources, improve the developer and user experience in the journey editor, and address minor UI details.

The console renders a "Sending" badge for a scheduled message whenever
`has_pending_events` is true. That field was computed as "any unfired
event exists", but recurring schedules pre-generate the next
occurrence's event the moment they advance (anchor + N*interval). As a
result every recurring schedule perpetually had an unfired future event
and was shown as "Sending" indefinitely, even while idle months before
its next run.

Scope `has_pending_events` to events that are actually due
(`fired_at IS NULL AND fire_at <= NOW()`) in ListUserSchedules and
ListOrganizationSchedules. This matches the existing scan-due predicate
and the "up to 1 minute to process" tooltip: the badge now reflects an
event being actively delivered, not one queued for a future occurrence.

No frontend change needed — the badge keys solely off this field.
Recurring schedules advanced to their next occurrence with a
generate_series(1, 10000) scan from the anchor. That scan both capped how
far a schedule could be advanced and was O(N) in the number of skipped
occurrences: a schedule dormant for more than 10000 intervals (e.g. a
1-minute interval idle for ~7 days, or hourly for ~1 year) could never
resume — the advance query returned no row and errored every cycle.

Replace the scan with a next_schedule_occurrence SQL function that
estimates the occurrence number in O(1) from the average interval length
and then corrects it exactly with a couple of interval-arithmetic loops.
The estimate only seeds the loops; the returned occurrence is exact for
any strictly-increasing interval and there is no upper bound on the
catch-up gap. Both the user and organization advance paths and
computeNextOccurrence now call it, removing the duplicated calendar math.

Tested: exact known cases (yearly, month-end clamping, boundary, ~470yr
dormancy), a 640-case property grid asserting the defining properties
across leap years / mixed calendar intervals / horizons to year 3025, and
rejection of non-positive intervals.
The create button stayed disabled after enabling and then disabling the
transactional toggle, even with a subscription selected. Toggling
transactional on cleared subscription_id, and the auto-select effect
restored a subscription via form.setValue without revalidating, leaving
formState.isValid stale at false.

Pass shouldValidate so the form revalidates when subscription_id is set
programmatically.
…are usable

The gate (and other downstream) variable picker only surfaces a step's
event variables when that step has a `data_key` — the backend likewise
only exposes step state under `journey.<data_key>.*`. Entrance steps never
got a data_key assigned automatically (it was a manual, easily-missed
field), so the triggering "input" event's variables never appeared in the
picker; only the always-present "User" group showed.

Give entrances a readable, unique default data_key ("entrance",
"entrance_2", …):
- at creation, in both the drag-drop and trigger-setup paths, and
- as a non-destructive backfill on load for entrances that predate this,
  persisted on the next manual save.
An exit step requires an `entrance_uuid` (the entrance it terminates), but
it was always empty on drop, forcing a manual pick and failing validation
until set. Default it to the first entrance when placed, so single-entrance
journeys — the common case — need no extra configuration.
The Journey editor's Send node renders the email preview inside a small
250x200 thumbnail (size="small"), but EmailFrame always laid out at full
scale: 22px subject, large avatar, generous padding, and a full row of
Gmail action icons. Crammed into the thumbnail this wrapped the subject,
sender name, and timestamp onto multiple lines and overlapped the reply
icon, leaving the preview looking broken.

Thread the existing `size` prop through to EmailFrame and add a compact
layout for "small": tighter padding, smaller subject/sender/avatar type,
truncation instead of wrapping, and hidden action icons. Full-size
previews are unchanged.
Replace the previous compact-header approach. For the small thumbnail in
the Journey Send node the Gmail-style header chrome (subject, sender,
action icons) doesn't fit and looked broken. Instead, render the
compiled email itself, clipped to the thumbnail height — a truer preview
of what will be sent. EmailFrame is reverted to its original full-size
layout; large previews are unchanged.
The generated oapi `HandlerWithOptions` wrapper composes middlewares with a
forward-wrapping loop (`handler = mw(handler)`), so the LAST entry in the
`Middlewares` slice becomes the outermost wrapper and executes FIRST. The
slice was ordered `[Validator, RateLimit]`, so RateLimit actually ran BEFORE
the validator's authentication.

As a result `rbac.FromContext` was always nil when the limiter computed its
key, and every request fell back to the `ip:` bucket. Behind a reverse proxy
or ingress (and `TrustedProxyHops` defaults to 0, so `RemoteAddr` is used),
that IP is the proxy's — identical for all traffic — collapsing the entire
deployment into a single shared 600/min bucket. API-key ingestion and
Clerk-authenticated console sessions then starved each other's budget instead
of being keyed independently per auth method.

List RateLimit before the validator on both surfaces so it runs *after*
authentication and keys on the resolved actor, and add a regression test that
composes the chain like the generated wrapper and asserts two actors from the
same IP get independent budgets.
…e scheduling (#280)

* feat(scheduled): address schedule assignments by id for multi-instance scheduling

Schedule assignments are now addressed by their own id. Submitting with an
existing assignment id upserts that row; omitting it creates a new instance, so
a single subject can hold multiple assignments to the same named schedule
(e.g. several reminders of the same kind, or rescheduling a specific one).

- migration drops the unique (user_id, schedule_id) / (organization_id,
  schedule_id) indexes; the non-unique lookup indexes are kept
- UpsertUserSchedule / UpsertOrganizationSchedule take an id and conflict on
  (id), guarded by user/schedule so a supplied id cannot hijack another
  subject's row (ErrScheduleOwnershipMismatch otherwise)
- ScheduledMsg carries assignment_id; the client API exposes an optional id on
  the upsert requests and returns the assignment id in the 202 response; the
  management API exposes an optional id so the console can target an instance
- journey schedule steps derive a deterministic assignment id from
  (user, schedule) so re-runs/retries stay idempotent (no behaviour change)
- regenerated client + management Go oapi and the console TS types

* feat(scheduled): journeys schedule a new instance per entry; client delete-by-id

Follow-up to the id-based assignment change:

- Journey schedule steps now create a NEW assignment per journey entry (a user's
  pass through the journey), so re-entering schedules an additional instance.
  The id is derived from (journey_entry_id, step_id) so a redelivered/retried
  step message stays idempotent instead of duplicating.
- Client API delete now accepts an optional id to remove a single assignment
  (ownership-checked); without an id it still deletes every assignment by name.
  name is now optional (either id or name required); regenerated client oapi.

* fix(scheduled): regenerate oapi with pinned oapi-codegen v2.7.0

The generated files were produced with v2.5.1 locally, which diverges from the
CI-pinned v2.7.0 and broke the generate check. Regenerated with v2.7.0; the diff
vs main is now scoped to the new schedule id fields only.

* fix(scheduled): keep omitted-id messages idempotent; require delete identifier

Addresses PR review:
- Consumer derives a deterministic assignment id from (subject, schedule) when a
  ScheduledMsg omits assignment_id, so producers that don't set one (user/org
  anniversary schedules) stay idempotent under at-least-once redelivery instead
  of minting a fresh id per delivery and duplicating the assignment. The client
  API still sends an explicit id, so its create-new-per-submit behaviour is
  unchanged.
- DeleteUserScheduledRequest now requires identifier in the schema (the handler
  already rejected requests without one); regenerated client oapi and updated the
  handler for the now non-pointer identifier.
- Added a unit test for resolveAssignmentID.

* docs(scheduled): update upsert doc comments for id-based semantics

The UpsertUserSchedule / UpsertOrganizationSchedule doc comments still described
the old (subject, schedule) conflict model. Updated to reflect that assignments
are keyed by their own id (nil creates a new instance; an existing id updates in
place, guarded by ErrScheduleOwnershipMismatch).
@jeroenrinzema jeroenrinzema merged commit ba055e4 into release/v0.1.0 Jun 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant