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-replacePUT, 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-configurationandPATCH /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}setsis_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 0the customer modal has always sent, so every avatar upload failed with "customer not found".
entity_id 0is now allowed forcustomer_avataras an explicit staging upload while the organization is
being created; a real id is still checked andcustomers.manageis 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
singleIS NULL/IS NOT NULLcondition 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 returnsinvalidated_atand 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
contextvariant 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, andscripts/check-deprecated-v1-api.shfails when
first-party source names the v1 base path outside the v1 implementation and a small allowlist. It runs in the
pre-commit hook andmake lint, somake ciblocks 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
CustomEventshape 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
surfaceErrorhelper, 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 loadsGET /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/wsHomebrew 7 requires trusting a third-party tap before installing from it:
brew trust windshiftapp/tapThe 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/v1keeps serving existingcrw_*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 isapi/openapi-v2.json. A lint guard blocks
new first-party v1 usage (bypass withV1_API_SKIP=1when 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, andJIRA_IMPORT_MAX_DURATION
(default 0, unlimited) caps an import and makes it reporttimed_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, addsportal_customers.languageandportal_customers.timezone, adds the
notification_template_translationstable for per-locale email overrides, addsitem_due_reminders.delivered
(defaultTRUE, 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
deliveredcolumn 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/meupdates the authenticated
customer'slanguageandtimezone; 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 legacycf_<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).