Windshift 0.9.2
Released 30 October 2026. Published as tag v0.9.2 on GitHub and as ghcr.io/windshiftapp/windshift:v0.9.2. Binaries and the macOS app are on the download page.
Windshift 0.9.2 makes the dashboard extendable and the board quicker to navigate. You can describe a custom widget in plain language and Windshift generates a validated, permission-aware program for it, backed by a component library of cards, metrics, tables, charts, and graphs. Boards gain multi-select bulk actions, keyboard navigation, saved filters, milestone swimlanes, and per-user views that follow you across devices. Alongside those, the release tightens portal, on-call, and cross-workspace authorization, makes email intake and outbound mail sturdier, adds optional S3-compatible attachment storage, and moves more work off the collection paint path.
Important upgrade notice: it is advised to update the ws CLI alongside this release for API compatibility. Read below for the fixes.
Highlights
Custom dashboard widgets
The dashboard is no longer limited to the built-in catalog. A new "Create custom widget" entry lets you describe what you want in natural language; the model produces an OpenUI program, you preview it at the real tile width, refine it, adjust the CQL queries, and save it.
The component library ships Card, Section, Title, Text, Divider, Badge, EmptyState, Metric, MetricRow, ItemsTable, ItemList, BarChart, LineChart, PieChart, ProgressList, ActivityFeed, and LinkButton, plus canvas-rendered network and matrix graphs.
Data access stays yours. Item queries resolve through the list service with the viewer's accessible-workspace list, asset queries resolve against a builder-selected asset set.
Custom widgets are model-generated, so the catalog offers them only when a model is configured and the feature is enabled. The AI pipeline meters generations against a per-user rate limit (20 generations / hour), validates the result with the real CQL parser and a dry run, and repairs positional errors before giving up.
Boards get even better
Boards get an explicit multi-select mode. Selection checkboxes appear only while the mode is active, selected cards stay opaque while the rest dim, and a sticky bulk toolbar carries status, assignee, priority, and label actions with a progress and error summary. You can also Shift-Click on a card to enter selection mode.
Per-user collection view state (show-completed, group-by, collapsed columns, sort mode, iteration filter, tree and map preferences, and personal saved filters) is now stored in user preferences rather than browser-local storage, so it follows you across devices. Existing local values remain readable as a fallback, and every new change is written to the server.
Milestone swimlanes
Boards can group by milestone, with a configuration dialog to pin global milestones and choose whether completed milestones are shown. Cards on hidden completed milestones collect into a single completed-milestones lane instead of disappearing, and a leading rescue lane surfaces open items whose primary milestone is completed or canceled so the mis-assigned work can be moved to the right milestone. The milestone picker is searchable, and empty global lanes stay hidden unless pinned.
Multi-value milestone fields
A new multi_milestone custom field type stores an array of milestone IDs, with validation, editing and rendering in the item sidebar, filtering, and CQL support. CQL also resolves bare custom-field names such as affectedVersion = 1 in addition to the cf_, custom., and cfid_ forms. This field serves primarily to migrate existing Jira issues more faithfully.
Attachments can live in S3
Attachment blobs can now be stored in any S3-compatible bucket instead of local disk. Set STORAGE_BACKEND=s3 together with S3_BUCKET and S3_REGION, and Windshift routes uploads, downloads, previews, and diagrams through the new blob store. Static credentials are optional, so AWS deployments can use the standard credential chain (environment, web identity/IRSA, instance profile); self-hosted stores such as MinIO, RustFS, and Wasabi set S3_ENDPOINT and usually S3_FORCE_PATH_STYLE=true. Downloads stay authorized and are proxied through Windshift with ranged reads, and /readyz reports bucket reachability from a cached probe. Local disk remains the default and existing deployments need no change. Thanks to Marco (@valentimarco) for this feature (#378).
Also in 0.9.2
- Sidebar order follows you. Workspaces in the sidebar are ordered by your most recent visits instead of a fixed order, and the recent order is preloaded before the menu opens so the list does not reshuffle as you navigate.
- Start and end dates as list columns. Collections can show the start and end date system fields as list columns alongside the existing date columns (GH #391, thanks to @Jayden-SysData).
- Comment sort order sticks. The newest-first toggle on comments is stored in your user preferences and loads with the rest of your settings instead of resetting on every visit.
- One request for global catalogs. A new
GET /api/v2/catalogs/global-bootstrapreturns item types, statuses, categories, priorities, and optional custom fields in a single response, and the global workspace scope hydrates from it instead of five separate calls. Hydrated user-display records are preserved across refreshes and only reset on a scope or auth change. - Item type icons in key badges. Homepage activity feeds carry item type metadata, and the shared dashboard row renders the colored type icon next to the key.
- Jira connections are recoverable. Saved connections can be re-verified with a replaced token, removal soft-deletes the connection so import jobs and ID mappings keep their provenance, and destructive import actions move behind a row menu with a soft "Remove from list" alongside the confirmed data deletion.
- Saved queues are editable again. Editing a saved queue sent the wrong content type and failed with a 415; the merge-patch request and the workspace default-view selector now both work (GH #386, thanks to @jetedv).
- Source-control credentials stay in sync. A refreshed OAuth grant is mirrored between the user and workspace copies, so a periodic repository sync no longer leaves the user copy dead and milestone releases failing (GH #387, thanks to @Jayden-SysData).
wsproject-config safety. A[server]section in a git-trackedws.tomlis ignored with an explanation, so a repository cannot redirect a token to its own host, while a user's own untracked file is still honored.ws config init --non-interactivenow readsWS_URLandWS_TOKENfrom the environment as documented.- Due-date reminders are opt-in. Assignees can turn on due-date reminders in their profile and choose how many days ahead (1–30) to be notified; reminders are never sent unless enabled. A background sweep writes one notification per item, due date, and assignee, so repeated ticks and restarts do not duplicate a reminder while a changed due date re-reminds. The sweep interval and batch size are configurable with
WINDSHIFT_REMINDER_INTERVALandWINDSHIFT_REMINDER_BATCH_SIZE(GH #392, thanks to @Jayden-SysData). - Kubernetes deployment example.
deploy/kubernetes.mddocuments a copy-and-adapt set of manifests underdeploy/kubernetes/, including the single-replica constraint, an SSE-safe ingress, and the shape of the required secret.
Performance
- Collection pages appear faster. The unfiltered item count is no longer computed while the page renders; it is deferred and filled in shortly after. The workspace directory is warmed in the background, card details load only when a card scrolls into view, field availability comes from the shared effective-config cache, and assignee names for a whole page load in a single authorized request.
- The event engine's idle ticks are gated with a single statement per tick, and consumer cursors are pruned against the active set every tick so inactive consumers do not leave stale entries (@Faks).
- The global workspace scope no longer clears hydrated users on refresh, and reference catalogs load in one request instead of five.
- Free-text item search is indexed. Title and description no longer fall back to a
LIKE '%term%'scan; token-prefix matches use an FTS5 index on SQLite and a GINtsvectorindex on PostgreSQL, so search stays fast as the item table grows. - Permission checks stop serializing. The permission snapshot cache stores typed snapshots in a sharded, count-bounded cache with a lazy TTL and background sweep instead of marshalling and decoding the whole snapshot on every check, and the Everyone fallback is stored once per process rather than duplicated per user.
- Notification fan-out decodes once per recipient. Recipient filtering resolves each recipient's permission snapshot once and batches the
is_activeprobe, instead of one decode and one query per recipient per event. - The mobile pages list and both mobile search surfaces resolve workspaces in one request per 50-workspace batch instead of one request per workspace, and the portal channel settings page reads connected mailboxes from a single channel-list request.
- Applying labels to a multi-item board selection sends one request instead of one per selected item.
- Rate limiting no longer serializes every request on a process-wide write lock: the unified limiter uses a sharded, count-bounded token-bucket map with an atomic last-seen timestamp.
Security
- The audit pipeline records each event's source (HTTP, SCIM, LDAP, MCP, SSH, and others), audits authorization denials at the permission and bearer middleware and in the v1/v2 admin helpers, audits v2 admin user/group mutations and item-attachment deletion, writes SCIM and LDAP rows synchronously, and preserves details on truncation.
- Login and other IP-keyed limiters no longer trust a client-controlled
X-Forwarded-For; forwarded headers are honored only from a trusted socket peer, walking the chain right to left. - Fractional select option IDs are rejected instead of silently truncated on asset and custom fields.
- bcrypt's 72-byte input limit is enforced on every password-setting path and at login, so an over-long password can no longer match on its first 72 bytes or produce an internal error.
- SSO display names resolve from mapped claims instead of twisting the display name into first and last name.
dompurifywas bumped to 3.4.17, and vulnerable demo-plugin and test dependencies were patched.- Condition scripts run in a fresh, isolated VM per execution. A pooled VM previously let a script mutate built-in globals and prototypes that leaked into later executions and other workspaces.
Bug fixes
- Outbound mail is built with
mime/multipartinstead of hand-formatted strings, so the boundary parameter, part headers, and delimiters cannot disagree; this fixes rejection by strict providers such as SES. SMTP display names are quoted, so a"Lastname, Firstname"sender no longer splits the recipient header and drops intake receipts (GH #372, thanks to @thegingerninja; GH #371, thanks to @jetedv). - Email intake and delivery are harder to stall. IMAP poll interval and batch size are configurable and mailboxes poll concurrently, a newly connected mailbox no longer turns its existing mail into tickets on the first poll, the watermark is preserved when a mailbox time budget expires, digest batches are claimed just before send so instances cannot send duplicates, outbound drains reuse connections and enforce per-message budgets behind an SMTP circuit breaker, and tracking rows are deleted in bounded batches.
- Portal Hub inbox endpoints work on PostgreSQL by decoding the portal slug in Go instead of using SQLite-only
JSON_EXTRACT. - The portal theme is scoped to the portal surface, so "Back to App" no longer leaks portal styling into the application.
- Milestone releases show the release date instead of the creation timestamp, and the time entry list reflects an edited worklog date.
- The reasoning level setting is honored on Chat Completions requests, and provider-native reasoning fields pass through verbatim and take precedence over the normalized setting.
- Milestone options are seeded only for the item's workspace, so switching items no longer locks the picker onto a stale empty list.
- Layout editing falls back to a
getRandomValues-based UUID whencrypto.randomUUIDis unavailable, so serving Windshift over plain HTTP works again (GH #356, thanks to @wpa1160). - Nested tree rows are visibly indented with the configured columns kept aligned, and the browser tab shows the item key and title while an item is open and restores the previous title on close.
- Pie widgets no longer draw Cartesian axis furniture, and a leading title that duplicates the tile heading is dropped.
- The non-destructive connection-removal copy is present in every locale, and pie legends sit beside the chart on wide tiles.
- A background refresh no longer discards your in-progress edits: an SSE update for the item already on screen merges over the active local edits instead of replacing the store row, and page-based list views reload the page you are on instead of jumping back to the first page.
- The daily-briefing scheduler registers a run only after the per-feature AI check passes, so a missing provider no longer leaves a phantom generation in diagnostics, and the widget no longer logs the normal "no briefing yet" state as a warning on every load.
- The milestone create dialog stacks above the board, and creating a milestone from it refreshes the picker so the new option appears immediately (GH #393, thanks to @Jayden-SysData).
- Page-scoped attachments are served from the shared binary endpoints, and a bearer token with
pages:readoritems:readis accepted for the matching attachment instead of being rejected with a 403 (GH #389, thanks to @thegingerninja). - The workspace access summary now explains that assigning the Viewer role restricts the workspace so only assigned users and groups can see it (GH #388, thanks to @JamiesonRoberts).
- Dependency bumps:
dompurify3.4.17,vitest4.1.12, andgithub.com/klauspost/compress1.20.3.
Upgrade notes
- Drop-in replacement for 0.9.1. Schema migrations add the
custom_widgetstable, add the due-date reminder preference columns, makequeues.workspace_idnullable for global collections, and drop the unused personal-label tables; they run automatically. No configuration change or re-login is required. - PostgreSQL 15 or newer is required. Startup refuses an older server with a clear message instead of failing later on schema features it lacks. SQLite deployments are unchanged.
- Personal labels were removed. The unused personal-label feature and its API fields are gone; workspace item labels are unchanged. The migration drops the
personal_labelsandpersonal_item_labelstables. - Passwords. Inputs longer than bcrypt's 72-byte limit are now rejected on every set path and at login. Accounts with such a password (only possible where the limit was not previously enforced) need an admin reset.
- S3 attachment storage (optional). Set
STORAGE_BACKEND=s3to move attachments to a bucket; seedeploy/.env.examplefor the full setting list. Existing local deployments need no change. When S3 is enabled,ATTACHMENT_PATHbackups no longer cover attachment blobs. Use the bucket's own versioning, replication, or backup mechanism. - Attachment uploads reject unknown
entity_typevalues with a 400 instead of silently storing them as item attachments. The built-in frontend only sends known types. - Item search is indexed. The
20261116_item_search_prefixmigration builds an FTS5 index on SQLite (rebuilding it from existing rows) or a GIN index on PostgreSQL withCREATE INDEX CONCURRENTLY, so writes are not blocked. On a largeitemstable the first upgrade can take a while and increases database size. The PostgreSQL index folds accents when theunaccentextension is available, installing it best-effort.
Thank you
Thank you to Daniel (@danielraab) for the nested-tree indentation fix and the item-detail document title (GH #361, GH #359), and to Oskars (@Faks) for gating the event engine's idle ticks with a single statement (#360). Thank you to Marco Valenti (@valentimarco) for the opt-in S3-compatible attachment storage backend (#378).