github Windshiftapp/core v0.9.4
Windshift v0.9.4

36 minutes ago

Windshift 0.9.4

Windshift 0.9.4 adds transition screens, so a workflow can require input before a status change instead of only guarding
it. A transition can now carry a screen of required and optional fields; a move is refused until the required fields are
satisfied, the values are written with the status change in one transaction, and the workflow designer gained a
transition inspector to bind, create, and edit those screens.

On the Jira side, imports now finish instead of stopping at an arbitrary two-hour mark, download attachments and
complete per-issue subresources concurrently, and no longer fetch per-issue watchers by default. Alongside those, portal
and organization custom field values are keyed by field id, the filter builder addresses custom fields by their unique
cfid_<id>, and several customer, board, filter, and notification bugs are fixed.

This release also makes a large pass at right-to-left languages and localization. Arabic was reviewed and corrected,
Persian is new, user text now renders with its own direction, and email follows the recipient's language. Arabic and
Persian are here to be tested and verified, not declared complete; see the right-to-left section for the current state.

Important upgrade notice: the v1 API (/rest/api/v1) is now marked deprecated and frozen. New integrations, clients,
and tooling must use v2 (/rest/api/v2), and a lint guard blocks new first-party v1 usage. If you run Jira imports,
review the new JIRA_IMPORT_* variables in the upgrade notes. It is also advised to update the ws CLI alongside this
release; the CLI can now be installed with Homebrew (see the Homebrew install section below).

Highlights

Transition screens

A transition can carry a screen, a set of fields collected before the move. The move is refused with screen_required
and a structured missing_fields payload until every required field is satisfied; optional fields and already-satisfied
required fields never block. The submitted values are overlaid onto the condition context so validators see them, and
the fields and the status change are applied in one transaction, so a rejected transition writes nothing (WI-2011,
WI-2012).

Every user-initiated move opens a shared transition modal when the target transition has a screen: item detail, the
board context menu and drag, mobile, personal tasks, and the weekly calendar. The modal renders the shared field form
and surfaces missing required fields inline. Bulk moves cannot collect screen input, so a batch that contains an item
whose target transition needs a screen is refused whole with 409 screen_required and the blocked items listed, instead
of partially applying and reporting the rest per item; the board and support queue offer a retry without them (WI-2013).

The workflow designer gained a right-hand transition inspector. Selecting an edge binds, unbinds, or creates and binds a
screen, saving immediately through PUT /transitions/{id}/screen, and a screen-bearing edge carries a badge. A screen
can be laid out in the inspector itself through the shared field editor, and a screen in use cannot be deleted. The
binding lives on workflow_transitions.screen_id, is returned by the transition governance endpoint, and is
deliberately kept out of the bulk graph-replace payload so a graph save never clobbers it (WI-2014).

Jira imports run to completion

The importer no longer runs under a hard-coded two-hour deadline. A larger project used to stop mid-import, fail its
remaining bulk fetches with a deadline error, count unprocessed issues as failures, and report a generic
completed_with_errors with no explanation. Per-request HTTP timeouts and the worker lease already bound the work, so
the cap is gone and a large project is allowed to finish. An operator can still set JIRA_IMPORT_MAX_DURATION; when a
configured limit is hit, the import stops before the next batch without counting unprocessed issues as failures and
reports an explicit timed_out status naming the limit and the imported/total counts (WI-1981).

The importer also stopped issuing every Jira request sequentially, which bound throughput to round-trip latency (about
1.8 requests/second observed) rather than the client's 10 requests/second limiter. A bounded worker pool (default 8,
JIRA_IMPORT_CONCURRENCY to override) now drives the two phases that dominated large imports: attachment downloads and
the per-issue comment/worklog/Jira Service Management completion calls. Reuse checks, record writes, and the shared
progress counters stay on the main goroutine, so the persisted state is unchanged (WI-1982).

Two more changes cut import time and noise. Per-issue watcher import is now off by default, because it made one
sequential Jira request per issue for a low-value mapping; set JIRA_IMPORT_WATCHERS to opt back in, and the watcher
count the bulk fetch already returned is still preserved (WI-1983). Attachment downloads now go through the same retry
helper as JSON requests, so a 429 or 5xx is retried with an honored Retry-After and jittered backoff instead of
dropping the file silently, and a per-client throttle pauses every worker for the Retry-After window rather than
stampeding the limit (WI-2010).

Custom fields are addressed by id

Portal customer and customer-organization custom field values were keyed by the human-readable field name, so duplicate
names collided and a rename orphaned the stored values. They are now stored by numeric field id, like items and assets,
with the legacy name key kept as a rollback mirror for a few releases. Existing rows are migrated, writes are validated,
and the delete guard and cleanup scheduler cover both JSON columns (WI-2025).

The filter builder had the same class of bug in a different place: custom filter fields took their id from the
cf_<name> alias, so two custom fields sharing a name produced identical option keys and the field picker crashed with
each_key_duplicate. Filter fields now use the backend's collision-free cfid_<id> name for custom fields; the legacy
alias is still accepted for saved queries (GH #454).

Right-to-left text, Arabic, and Persian

Arabic and Persian are the focus of a large localization pass in this release, and it is explicitly a review pass. Both
locales are here to be used, tested, and verified, not declared finished. Expect rough edges in layout and wording, and
treat them as under review.

User text follows its own direction. User-generated text inherited the interface direction, so an Arabic title was
left-aligned in a right-to-left interface and mixed-language phrases reordered around it. Item titles, descriptions,
comments, custom-field values, portal request titles, dashboard rows, and list, board, roadmap, milestone, and iteration
titles now render with dir="auto" and align to their resolved direction. The shared text primitives (Badge, Chip,
Lozenge, Text, TruncatedFieldValue) follow their content direction, item keys stay left-to-right as code, and Input and
Textarea default to dir="auto" so typed text follows what is typed. A .bidi-isolate utility handles inline
mixed-direction runs, and a follow-up sweep covered data tables, the command palette, the rich-text editor root, and
one-off name sites in selects and chips (GH #461, WI-2038).

Arabic was reviewed and corrected. The Arabic locale carried literal machine-translation artifacts: a runner became
"racer", Notion became "the idea", Mermaid became the mythical creature, a permissions principal became "school
principal", and an approver pool became "swimming pool". Those, along with garbled sentences, untranslated English
blocks, and terminology drift, were corrected, and the remaining English fallbacks were filled so the locale covers
every key. Terminology was settled on milestone = معلم, labels = تسميات, backlog = قائمة المهام المعلقة, slug = المعرّف
المختصر, and organization = مؤسسة (WI-2035).

Persian is new and still in review. Persian (fa) ships as a right-to-left locale, mirrored key for key from English,
listed in the language picker, and negotiated from fa-* browsers. A terminology review corrected machine-translation
artifacts and filled empty help strings. The date picker is calendar-aware: it renders the Persian calendar with the
locale's week start and numerals, while the stored value stays a Gregorian YYYY-MM-DD string, so storage and APIs are
unchanged. The locale gate requires full key coverage so Persian cannot silently fall back to English, but its layout
and copy are still being verified (WI-2033).

Email follows the recipient. Transactional and notification emails are no longer English-only: built-in copy, digest
content, and dates render in the recipient's language and timezone, portal customers can choose their language, and
admins can override email templates per locale (WI-2034).

The Simplified Chinese (zh-CN) locale also received a mistranslation and terminology review (WI-2037).

Also in 0.9.4

  • Board configuration merges instead of replacing. The navigation page toggled a board's view settings by
    round-tripping the whole configuration through the full-replace PUT, which the strict v2 decoder rejected because
    the fetched columns carry server-managed fields, so every toggle failed with "Request body is not valid JSON", and the
    reset path zeroed columns, card fields, list columns, backlog statuses, and more. New
    PATCH /collections/{id}/board-configuration and PATCH /workspaces/{id}/board-configuration
    endpoints apply merge patches, writing only the fields present and reusing the existing write path
    for authorization, validation, view-settings merging, auditing, and create-on-missing
    (GH #456).
  • Attachments can be hidden from portal customers. A new PATCH /attachments/{id} sets is_internal, so an editor
    can hide or reveal an item attachment after upload instead of only at upload time (WI-2032).
  • Organization avatars upload again. An existence check added to attachment uploads rejected the staged
    entity_id 0 the customer modal has always sent, so every avatar upload failed with "customer not found".
    entity_id 0 is now allowed for customer_avatar as an explicit staging upload while the organization is
    being created; a real id is still checked and customers.manage is required either way. The modal also sends
    the edited organization's id, so edit uploads name their owner (WI-2028).
  • Organization custom fields are labeled. The Edit Organization dialog rendered each custom field without a label,
    so a bare picker (for example a user field) gave no hint about which field it edited; each field now has its own
    label, and the sidebar heading uses the standard section style (WI-2029).
  • A lone null filter is active. The filter builder only treated a row as applicable when it carried a value, so a
    single IS NULL / IS NOT NULL condition left Apply and Save disabled. A null operator now counts as active in the
    filter menu and dynamic filter popover (GH #452).
  • Rejected integration tokens prompt a reconnect. The connected-accounts surface listed a rejected connection as
    still connected. The connections endpoint now returns invalidated_at and the shared integrations list renders a
    "Reconnect required" badge with a Reconnect action, so a refresh-token provider prompts the user instead of silently
    failing search (WI-2024).
  • A context button variant. The Button component gained a context variant that follows the --ctx-* theme tokens,
    now used by the hierarchy level picker trigger.

Security

  • The v1 API (/rest/api/v1) is marked deprecated and frozen, and scripts/check-deprecated-v1-api.sh fails when
    first-party source names the v1 base path outside the v1 implementation and a small allowlist. It runs in the
    pre-commit hook and make lint, so make ci blocks it.
  • Attachment visibility changes are audited as attachment.visibility.

Bug fixes

  • The transition inspector opens and stays usable. The designer's edge-click handler read a CustomEvent shape the
    library never sends, so the inspector never opened; edges drawn in the session can now stage a screen until the save
    assigns the transition its id, the inspector's controls stay in the viewport, and screen field counts are accurate
    before the first save (WI-1835).
  • Partial updates no longer clear omitted fields. The portal customer, customer-organization, portal
    regional-settings, and SSO provider update endpoints decoded non-pointer request fields and wrote every column, so a
    partial payload silently cleared values the shipped clients never send: the primary-contact flag, organization
    request-sharing settings, the portal timezone, and SAML request signing. An omitted field now keeps its stored value;
    create still applies its documented defaults.
  • Silent frontend failures now surface. User-triggered actions across item detail, portal, settings, dialogs, tests,
    calendar, mobile, and SCM that previously only logged to the console now raise an error toast through a shared
    surfaceError helper, with a global safety net for unhandled rejections and window errors. Intentionally silent flows
    such as draft autosave and the SSE reconcile stay silent (WI-2039).
  • The profile page loads the signed-in user's full record. It sourced the current user from the v2 user-directory
    summary, which omits email, timezone, and language, so the email rendered blank until a regional-settings save
    replaced the record and the language and timezone pickers fell back to their defaults. It now loads GET /users/me
    (GH #462, GH #463).
  • Due-date reminders are more resilient. The sweep no longer reminds assignees about items that were merged into a
    canonical ticket or that live in an inactive workspace, and it includes the whole final lead day when a due date
    carries a time component. A single recipient whose authorization lookup or notification scan fails no longer aborts
    the whole email digest: that recipient is skipped and retried, and the tick reports an error only when every candidate
    failed. The reminder ledger now records whether a reminder was delivered, so a skipped recipient is retried after 24
    hours instead of being dropped while a delivered row is never downgraded.
  • The notification tray stays in sync. Marking notifications read or seen, and clearing them, now broadcast a
    change so an open tray reconciles instead of going stale. Dismissing a single notification is persisted through
    DELETE /notifications/{id} instead of being local only, so it cannot reappear or still trigger the email digest.
    Korean gained the reminder settings strings.

Install the ws CLI with Homebrew

The ws command-line client is available through Homebrew:

brew install windshiftapp/tap/ws

Homebrew 7 requires trusting a third-party tap before installing from it:

brew trust windshiftapp/tap

The formula covers macOS and Linux on arm64 and amd64. Windows is not covered (WI-1993).

Upgrade notes

  • The v1 API is deprecated and frozen. /rest/api/v1 keeps serving existing crw_* bearer tokens and receives
    security fixes only, but no new routes or integrations may be built on it. New work must use v2: bearer clients call
    /rest/api/v2, session clients /api/v2, and the canonical contract is api/openapi-v2.json. A lint guard blocks
    new first-party v1 usage (bypass with V1_API_SKIP=1 when a legitimate exception is needed).
  • Jira imports gained three environment variables. JIRA_IMPORT_WATCHERS (default off) opts back into per-issue
    watcher import, JIRA_IMPORT_CONCURRENCY (default 8) sets the worker pool size, and JIRA_IMPORT_MAX_DURATION
    (default 0, unlimited) caps an import and makes it report timed_out.
  • Schema migrations run automatically. This release adds workflow_transitions.screen_id (ON DELETE RESTRICT),
    rekeys portal-customer and customer-organization custom field values to numeric field ids with the legacy name key
    kept as a mirror, adds portal_customers.language and portal_customers.timezone, adds the
    notification_template_translations table for per-locale email overrides, adds item_due_reminders.delivered
    (default TRUE, so existing rows keep their behavior), and adds PKCE and refresh-token columns to the integration
    OAuth tables. No configuration change or re-login is required.
  • Due-date reminder delivery is tracked. The reminder ledger's delivered column lets the sweep retry a skipped
    recipient after 24 hours.
  • Notification dismissal is persisted. A new DELETE /notifications/{id} removes one notification for the
    authenticated user.
  • Portal customers can set their own language and timezone. PUT /portal/{slug}/auth/me updates the authenticated
    customer's language and timezone; an omitted field keeps its stored value. The browser language is captured once
    on first contact, so even the first magic link is localized.
  • Filter fields address custom fields by cfid_<id>. Saved queries that use the legacy cf_<name> alias still
    resolve; only the ids the builder emits changed.

Thank you

Thank you to Toofan Mohammadi (@mrtfn) for the Persian/Jalali calendar support
(#478).

Thank you to Michel Jelsma (@mijelsma) for the PR honoring configured completed statuses in the
backlog (#395).

Don't miss a new core release

NewReleases is sending notifications on new releases.