github Windshiftapp/core v0.9.2
Windshift v0.9.2

4 hours ago

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-bootstrap returns 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).
  • ws project-config safety. A [server] section in a git-tracked ws.toml is 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-interactive now reads WS_URL and
    WS_TOKEN from 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_INTERVAL and
    WINDSHIFT_REMINDER_BATCH_SIZE (GH #392, thanks to
    @Jayden-SysData).
  • Kubernetes deployment example. deploy/kubernetes.md documents a copy-and-adapt set of
    manifests under deploy/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 GIN tsvector index 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_active probe, 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.
  • dompurify was 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/multipart instead 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 when crypto.randomUUID is
    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:read or items:read is 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: dompurify 3.4.17, vitest 4.1.12, and github.com/klauspost/compress
    1.20.3.

Upgrade notes

  • Drop-in replacement for 0.9.1. Schema migrations add the custom_widgets table, add the due-date
    reminder preference columns, make queues.workspace_id nullable 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_labels and
    personal_item_labels tables.
  • 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=s3 to move attachments to a bucket;
    see deploy/.env.example for the full setting list. Existing local deployments need no change.
    When S3 is enabled, ATTACHMENT_PATH backups no longer cover attachment blobs — use the bucket's
    own versioning, replication, or backup mechanism.
  • Attachment uploads reject unknown entity_type values 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_prefix migration builds an FTS5 index on
    SQLite (rebuilding it from existing rows) or a GIN index on PostgreSQL with CREATE INDEX CONCURRENTLY, so writes are not blocked. On a large items table the first upgrade can take a
    while and increases database size. The PostgreSQL index folds accents when the unaccent
    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).

Don't miss a new core release

NewReleases is sending notifications on new releases.