Windshift 0.9.2
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 also update your ws cli if you are using it with the latest
Windshift version to ensure 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_URLand
WS_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 withWINDSHIFT_REMINDER_INTERVALand
WINDSHIFT_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 theis_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-onlyJSON_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/compress
1.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 thepersonal_labelsand
personal_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 theunaccent
extension 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).