Skip to content

fix(charts): limit ideal burndown tooltip precision - #9968

Open
HaiYangBG1 wants to merge 57 commits into
makeplane:previewfrom
HaiYangBG1:fix/8706-burndown-tooltip-precision
Open

HaiYangBG1 wants to merge 57 commits into
makeplane:previewfrom
HaiYangBG1:fix/8706-burndown-tooltip-precision

Conversation

@HaiYangBG1

@HaiYangBG1 HaiYangBG1 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Description

The burndown tooltip renders ideal work-item counts at full floating-point precision, e.g. 8.571428571428573. Display the ideal series with at most one decimal place (8.6), while keeping integers and zero compact.

Add an optional numeric tooltip formatter to AreaChart and opt in only for ProgressChart's ideal series. Current work-item/estimate values retain their precision, and chart data and other chart defaults stay unchanged.

Type of Change

  • Improvement (change that would cause existing functionality to not work as expected)

Test Scenarios

Current head 052f6e7318e3347fc3439da6d343ff4de26d313a includes preview at efa46d2840cee6872f838cb07fade205d05345c0. The tooltip uses upstream's TooltipPopup and typography while retaining the optional numeric formatter. Callback parameters in the touched chart file were renamed to satisfy the current pre-commit no-shadow rule.

Validation on macOS ARM64, Node 22.22.2 and pinned pnpm 11.10.0:

  • Full @plane/blocks unit/DOM suite: 36 files, 386 tests passed.
  • The four mouse-hover regressions passed again after the callback rename.
  • Scoped web/blocks/types type checking passed, including dependency builds.
  • Scoped lint and formatting passed with the repository's warning thresholds.
  • Normal pre-commit formatting and lint checks passed without bypass.
  • git diff --check origin/preview passed.

The existing before/after browser proof shows the ideal tooltip changing from 8.571428571428573 to 8.6, with the current count remaining 9 and unchanged SVG curves. It predates this integration. The latest checks exercise the real updated chart component; the full authenticated application flow was not rerun.

Hosted build/lint, CodeQL, React Doctor and copyright checks passed on this head. CodeRabbit's latest review reported no actionable comments. Maintainer review and merge remain separate.

References

Closes #8706.

Summary by CodeRabbit

  • New Features
    • Area chart tooltips can now format numeric values selectively by data series.
    • Progress chart tooltips display values for the ideal series rounded to one decimal place.
  • Tests
    • Added coverage for selective formatting, unchanged chart data, and preserving whole-number display without a decimal suffix.

sriramveeraghanta and others added 29 commits April 6, 2026 20:00
…ifications

issue-updates.html rendered each new-comment value with {{ actor_comment|safe }},
which disables Django's autoescaping entirely. Since actor_comment is sourced
from comment.comment_html - raw, unsanitized user input - a comment containing
a payload like <img src=x onerror="..."> was written straight into the
notification email and would execute in any email client that renders inline
HTML/JS, as demonstrated with a live PoC in makeplane#9218 (CWE-79, reported 2026-06-05,
still open).

Neither this path nor the sibling "mention" path sanitizes comment content
before this point - process_html_content()/process_mention() in
email_notification_task.py only resolves @mention placeholders into plain
text, it isn't a sanitizer, and comments (as opposed to mentions) don't even
go through that step.

Switch to the built-in `striptags` filter, one of the fix options already
suggested in makeplane#9218. This strips all HTML (including any tag's attributes, so
an onerror handler never reaches the output at all) and leaves the residual
text through Django's normal autoescaping, which closes the hole without
adding a new dependency. Verified against Django directly: the PoC payload
from makeplane#9218 renders as executable HTML with |safe and as an empty string with
|striptags.

Comments will render as plain text in this email rather than keeping bold/
italic/link formatting - a deliberate, conservative tradeoff. A proper
allowlist sanitizer (e.g. bleach) could preserve that formatting safely, but
picking the right allowed-tags policy needs to match what the rich text
editor actually emits, which is a decision for the maintainers; a hand-rolled
allowlist isn't something to guess at in a security fix.
…up endpoints (makeplane#9335)

* fix: add rate limiting to email/password sign-in and sign-up endpoints

All four password authentication views (app sign-in, app sign-up, space
sign-in, space sign-up) extended django.views.View, so DRF's global
AnonRateThrottle never ran and the endpoints accepted unlimited credential
guesses with no friction (brute-force / credential stuffing, GHSA-349j).

Add authentication_throttle_allows(request) at the top of each post()
method — before any DB access — using the same AuthenticationThrottle
already guarding the magic-code views. On rejection the view redirects with
RATE_LIMIT_EXCEEDED, consistent with all other throttled auth endpoints.
Default limit remains 10/minute, overridable via AUTHENTICATION_RATE_LIMIT.

Co-authored-by: Plane AI <noreply@plane.so>

* refactor: consolidate auth throttle into a decorator + add tests

- Extract the repeated throttle-and-redirect block from the six redirect-flow
  auth views (email + magic, app + space) into a single throttle_auth_redirect
  decorator in rate_limit.py. Behaviour is unchanged: the throttle still runs
  before any DB access; brute-force traffic is rejected without a DB hit.
- Add regression tests for the password sign-in/sign-up throttle on both app
  and space endpoints, mirroring the existing magic-code throttle tests.
- Reset the shared AuthenticationThrottle bucket before every test in
  test_authentication.py. All auth endpoints share one per-IP throttle scope,
  so the newly-throttled password requests exhausted the budget mid-file and
  caused unrelated tests to trip RATE_LIMIT_EXCEEDED.

---------

Co-authored-by: Plane AI <noreply@plane.so>
Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
(cherry picked from commit e30605d)
…ane#9382)

* [WEB-8103] fix: stop leaking webhook HMAC secret_key on reads (GHSA-83rj)

WebhookEndpoint list/retrieve/patch pass a fields= allowlist that excludes
secret_key, but DynamicBaseSerializer.__init__ discards the caller
allowlist (fields = self.expand). With WebhookSerializer using
fields="__all__" and secret_key only in read_only_fields (read-only is
still serialized), the HMAC signing secret leaked on every webhook read
(GHSA-83rj-4282-x39v; admin-only).

Rather than secret_key = CharField(write_only=True) — which would let a
client inject their own secret on create/patch and break the intended
one-time reveal — hide it by default and reveal only where intended:

- WebhookSerializer.to_representation drops secret_key unless the
  show_secret_key context flag is set (secure by default). secret_key
  stays server-generated (default=generate_token) and non-writable.
- POST create and WebhookSecretRegenerateEndpoint pass show_secret_key so
  the secret is still returned once for the caller to configure their
  receiver; list/retrieve/patch no longer emit it.

Add contract regression tests (fail-before verified). Follow-up: the
DynamicBaseSerializer.__init__ allowlist bug affects other serializers —
tracked separately.

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8103] test: address review — patch network boundary + pin to_representation

Per review (@sriramveeraghanta):
- Patch the network boundary (validate_url) instead of the whole private
  _validate_webhook_url method, so the domain/schema checks still run and
  the test survives a rename of the private method.
- Add an assertion that an explicit fields=("secret_key",) request still
  hides the key, pinning to_representation as the enforcement point so a
  future DynamicBaseSerializer._filter_fields fix can't silently re-open
  the leak.

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8103] docs: spell out both levels of the dead fields= allowlist

The previous comment named only DynamicBaseSerializer.__init__ discarding the
caller's fields=, which is half the root cause. _filter_fields never removes
anything either: it builds `allowed` purely to attach expansion serializers for
names not already on the serializer, then returns self.fields unfiltered
(serializers/base.py:45-119).

So the fields= kwargs in views/webhook/base.py are no-ops on two independent
levels. Documented so a future `fields = fields or self.expand` fix isn't
assumed to re-activate the allowlists for confidentiality — _filter_fields has
to be made restrictive first. The show_secret_key context flag remains the sole
enforcement point.

Addresses @sriramveeraghanta's review on makeplane#9382. 6/6 contract tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(security): drop advisory identifiers from code comments

Explanations kept unchanged; only the IDs are removed.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3052792)
…eplane#9387)

* [WEB-8110] fix: sanitize page list order_by against an allowlist (GHSA-2v48)

PageViewSet.get_queryset passed the raw order_by query param into
.order_by(). In Django 4.2 .order_by() resolves field names at call time,
so an unknown field (e.g. order_by=password) raises FieldError → 500 DoS,
and a valid relation path (e.g. order_by=owned_by__password) enables ORM
relational traversal (GHSA-2v48-qcjw-74ch).

Add PAGE_ORDER_BY_ALLOWLIST to utils/order_queryset.py and wrap the param
with the existing sanitize_order_by() before it reaches .order_by(),
matching the issue/project/view/notification endpoints. Unknown or
malformed values fall back to the safe -created_at default.

Covers only the app project-pages residual; the 3 external-REST-API sites
in the advisory are handled by PR makeplane#9348. EE Wiki counterpart: WEB-8111.

Add contract regression tests (fail-before verified).

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8110] fix: fold sanitized order_by into a single order_by() call (review)

Address CodeRabbit + Copilot on makeplane#9387: the sanitized .order_by(user) was
immediately overridden by a later .order_by("-is_favorite", "-created_at"),
so the order_by param had no effect on the result (dead code) and cost an
extra query-build step.

Merge them into one .order_by("-is_favorite", <sanitized>, "id") — matching
the EE project-pages viewset — so favourites stay pinned first, the
allowlisted user ordering actually applies as the secondary sort, and id is
a stable pagination tiebreak. The no-param default is unchanged
(-created_at). Add a test asserting order_by=name / -name actually reorders
the results.

Co-authored-by: Plane AI <noreply@plane.so>

* chore(security): drop advisory identifiers from code comments

Explanations kept unchanged; only the IDs are removed.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
(cherry picked from commit d25b99c)
…akeplane#9442)

* [WEB-8283] fix: bind Spaces board object IDs to the anchor's project

The public Spaces board endpoints resolved the DeployBoard from the URL
anchor but trusted the caller-supplied issue_id/comment_id/intake_id
verbatim, without verifying the object belonged to that board's
project/workspace. Any authenticated user could write comments,
reactions and votes onto arbitrary issues cross-tenant, and read EXTERNAL
comments from a different project in the same workspace.

Bind every caller-supplied object id to the board's project + workspace
before writing:
- comment / issue-reaction / vote create: require the issue to exist in
  the board's project via Issue.issue_objects (excludes draft/archived/
  triage), else 404.
- comment-reaction create: require the comment to exist in the board's
  project with access="EXTERNAL", else 404.
- intake create: require the URL intake_id to match the board's intake,
  else 400.
- comment list read: scope the queryset to the board's project_id.

Also add the missing is_votes_enabled gate on vote create for parity with
comment/reaction create (pre-existing gap in the same method).

Adds contract regression tests (fail-before verified): cross-tenant
writes and the cross-project comment read now rejected, with positive
controls confirming legitimate board writes/reads still succeed.

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8283] test: address Copilot review — cross-workspace + votes-disabled coverage

- Add cross-workspace write test (issue in a different workspace) to
  exercise the workspace_id binding, matching the advisory's cross-tenant
  impact (previously only same-workspace/different-project was covered).
- Add a regression test for the new is_votes_enabled gate on vote create
  (votes-disabled board → 400), preventing the pre-existing gap from
  reappearing.
- Clarify the section header comment: cross-tenant writes return 404 for
  issue/comment binding, 400 for the intake binding mismatch.

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8283] refactor: extract board-scope guards into shared helpers

Address CodeRabbit review: the identical "object belongs to the board's
project+workspace" existence check was duplicated across four create()
methods (in four different ViewSets). Extract two module-level helpers —
_issue_in_board_scope and _comment_in_board_scope — so the check is a
single source of truth and cannot drift between endpoints or be forgotten
on a new one (the exact class of bug this PR fixes).

Behavior-preserving; 14 contract tests still green.

Co-authored-by: Plane AI <noreply@plane.so>

* chore(security): drop advisory identifiers from code comments

Explanations kept unchanged; only the IDs are removed.

Co-authored-by: Plane AI <noreply@plane.so>

* fix(security): use the deploy board's project_id in the reaction-create activity log

CommentReactionPublicViewSet.create() logged the activity with
str(self.kwargs.get("project_id", None)) — this route's URL only ever
supplies anchor and comment_id, never project_id, so every comment
reaction created on a public board logged project_id="None", silently
corrupting the activity/audit trail. destroy() on the same viewset
already resolves the correct project_id from the deploy board; create()
now does the same.

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8283] fix: apply board-scope guards to comment/reaction read, update and delete paths

The board/issue/external-comment scoping added by this PR's create() methods
was never applied to the list, update and delete paths built on the same
models. A caller could read reactions on an INTERNAL (non-public) comment
through the public reaction list, or reach a comment or reaction they
authored through a board it doesn't actually belong to via partial_update()
or destroy(), since those methods looked up objects by pk/actor only.

Bind IssueCommentPublicViewSet.partial_update()/destroy() to the board's
project, workspace, issue_id and EXTERNAL access; bind
IssueReactionPublicViewSet.destroy() to the board's project (previously only
workspace-scoped); and bind CommentReactionPublicViewSet.get_queryset()/
destroy() to EXTERNAL comments only. Add regression coverage for each gap,
plus positive controls confirming legitimate reads/writes on the board's own
objects still work.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
(cherry picked from commit 5b5af0a)
…akeplane#9466)

* [WEB-8352] fix(security): scope SubIssuesEndpoint to the URL project (GHSA-gxhv-fw9x-2pg3)

SubIssuesEndpoint is guarded only by ProjectEntityPermission, which verifies
the caller belongs to the URL project_id but not that the path issue_id lives
in that project. Both handlers then resolved issues without a project scope:

- GET filtered sub-issues by parent_id + workspace__slug only, leaking the
  names/priorities/assignees/dates of another project's sub-issues (read IDOR).
- POST loaded the parent by bare pk (no workspace/project scope) and filtered
  the moved sub-issues by workspace__slug only, letting any project member
  re-parent issues from other projects/workspaces (write IDOR).

Scope the parent lookup and both sub-issue querysets to the URL project_id
(and bind the parent to the workspace), returning 404 when the parent is not
in the caller's project. Adds 5 contract tests (3 security, 2 positive
controls); fail-before verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [WEB-8352] fix: dispatch sub-issue activity only for project-scoped issues (CodeRabbit/Copilot makeplane#9466)

The DB update + response were scoped to the URL project, but the activity loop
still iterated the raw caller-supplied sub_issue_ids. A cross-project id
(excluded from the re-parent) would still fire issue_activity.delay, whose task
does an unscoped Issue.objects.get and bumps updated_at — touching a foreign
issue and creating a bogus activity row.

Dispatch from the project-scoped sub_issues (scoped_sub_issue_ids) instead.
Strengthened the test to assert the foreign issue is absent from the response
body (sub_issues / state_distribution) and that no activity is dispatched for it
(mock). Fail-before verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(security): drop advisory identifiers from code comments

Explanations kept unchanged; only the IDs are removed.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Plane AI <noreply@plane.so>
(cherry picked from commit 3478d4f)
…ently dropping invalid ids (makeplane#9526)

Work item create/update via the external API returned 200/201 even
when assignees or labels didn't belong to the project, quietly
filtering the invalid ids out with no error. Now raises the same kind
of ValidationError already used for state/parent, matching the pattern
of makeplane#9517.

(cherry picked from commit aacfaac)
makeplane#9763)

* fix(api): avoid re-crawling issue link on update when url is unchanged

* fix(api): avoid recrawling unchanged issue links

(cherry picked from commit 1fec307)
… removed Docker Hub org (makeplane#9829)

MinIO removed the `minio` organisation from Docker Hub. Every deployment
that pulls `minio/minio` or `minio/mc` now fails with:

    pull access denied for minio/minio, repository does not exist
    or may require 'docker login'

The registry returns 401 (not 404) to anonymous clients, so the error
reads like a credentials problem when the repository is simply gone.

quay.io is MinIO's own second registry and still serves both images
publicly (multi-arch: amd64, arm64, ppc64le). Repoint every default at
quay.io, and pin an explicit RELEASE tag instead of `:latest` so a
future upstream change to a floating tag cannot break deployments the
same way:

  minio -> quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z
  mc    -> quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z

These are the digests `:latest` currently resolves to on quay.

Note: the community MinIO build is effectively frozen upstream (last
community push was 2025-09-07), so this unblocks deployments but does
not change the longer-term need to steer self-hosted users toward
external S3, with bundled MinIO as a dev/eval convenience.

Claude-Session: https://claude.ai/code/session_01TB2aRDJ5BMx6m5c7jDzkqM

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f25814c)
…akeplane#9443)

* [WEB-8289] fix: scope draft-to-issue conversion to the draft owner (GHSA-vfqm-7rh7-84hq)

WorkspaceDraftIssueViewSet.create_draft_to_issue resolved the draft with
get_queryset().filter(pk=draft_id) — scoped only to the workspace, no
created_by check — while the decorator admits any workspace ADMIN/MEMBER.
Drafts are private to their creator, so any member could pass another
user's draft_id to convert that draft into an issue, reassign (steal) its
file assets, and delete the victim's draft.

Scope the lookup to created_by=request.user (mirroring retrieve /
partial_update) and 404 when no owned draft matches. The explicit null
guard also removes a latent AttributeError on the following .project_id
access when the draft is not found.

Adds contract regression tests (fail-before verified): a non-owner
member is rejected with 404 and the draft + its assets are left intact,
with a positive control confirming the owner can still convert.

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8289] test: make fixture ownership real (address Copilot review)

Project and FileAsset fixtures passed created_by=create_user to
objects.create(), but BaseModel.save() nulls created_by when there is no
current request user (tests), so the ownership claim was silently a
no-op. Instantiate + save(created_by_id=...) instead — same pattern
already used for the draft — so the fixtures are accurate.

Co-authored-by: Plane AI <noreply@plane.so>

* chore(security): drop advisory identifiers from code comments

Explanations kept unchanged; only the IDs are removed.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
(cherry picked from commit a896115)
…invited project (makeplane#9866)

`ProjectJoinEndpoint.post` looked up the accepting user's existing
`ProjectMember` row by `(workspace_id, member)` and omitted `project_id`.
Accepting an invitation to project Y therefore reactivated whatever membership
row the user already had in the workspace -- typically a project they had been
removed from -- while project Y itself never received a membership row at all.

A companion no-op, `project_member.role = project_member.role`, left the stale
row at its original role, so a user removed as ADMIN from another project came
back as ADMIN even though the new invite was for a GUEST.

Scope the lookup to `project_id` and apply `project_invite.role` on reactivation.
The correct, project-scoped pattern already existed in
`UserProjectInvitationsViewset.create` in the same file.

Adds three regression tests covering: the invited project receives the
membership, an unrelated membership is not reactivated, and a rejoin applies the
invitation's role rather than the stale one.

(cherry picked from commit bae037c)
…akeplane#9734)

* chore: upgrade pnpm to 11.10, turbo to 2.10.11, patch postcss-selector-parser

- packageManager: pnpm@11.3.0 -> pnpm@11.10.0
- turbo catalog: 2.9.18 -> 2.10.11 (latest)
- override postcss-selector-parser >=7.1.0 <7.1.3 to 7.1.3 (dependabot
  alert makeplane#345, DoS via uncontrolled AST recursion)

* chore: remove comment from postcss-selector-parser override

(cherry picked from commit 3717500)
Bumps the npm_and_yarn group with 1 update in the / directory: [sanitize-html](https://github.com/apostrophecms/apostrophe/tree/HEAD/packages/sanitize-html).

Updates `sanitize-html` from 2.17.5 to 2.17.7
- [Changelog](https://github.com/apostrophecms/apostrophe/blob/main/packages/sanitize-html/CHANGELOG.md)
- [Commits](https://github.com/apostrophecms/apostrophe/commits/HEAD/packages/sanitize-html)

---
updated-dependencies:
- dependency-name: sanitize-html
  dependency-version: 2.17.7
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit cb28b70)
(cherry picked from commit 97ea97e, adapted: web/admin keep nginx on this branch, so only the APK security-upgrade layer is applied there)
…#9806)

* fix(security): clear the 2026-09-10 nightly Trivy findings

Two advisories published 2026-09-08 21:2x UTC reached Trivy's DB between
the 09-09 nightly (green on every plane-* leg) and the 09-10 one, turning
plane-live-community and plane-space-community red on unchanged images.

- js-yaml 4.3.1 -> 4.3.2 (CVE-2026-84375). The exact pin was the patched
  version at the time and became the vulnerable one; the comment now says to
  bump rather than widen it, since a range drifts onto the 5.x break.
- sharp ^0.35.3 -> ^0.35.4 (GHSA-rgj7-g3m4-5g8c, libheif GHSA-g89c-p67h-r497
  and GHSA-2jg2-4ch7-h545).

The third finding on plane-proxy/admin/frontend-community, CVE-2026-84445 in
the Caddy binary's grpc-go, needs no change here: 2.11.4 is the newest Caddy
published, and the nightly scan reads plane-ee's .trivyignore.yaml for every
leg, where it is suppressed at **/bin/caddy on the reachability argument that
Caddy runs no gRPC xDS server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: drop the added comment from pnpm-workspace.yaml

Review feedback from @sriramveeraghanta. The diff is now two version changes and
nothing else; the advisory ids and the reasoning stay in the PR description.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2f895b8)
- vitest / @vitest/mocker: bump catalog to ^4.1.11 (path traversal /
  arbitrary file read via redirect mock)
- qs: pin override to 6.16.0 (DoS via attacker-controlled isBuffer,
  array-limit bypass via bracket-key comma parsing)
- morgan: pin override to 1.12.0 (log forging via unescaped Unicode
  line separators)
- postcss-selector-parser: pin the 6.1.x line to 6.1.3 (DoS via
  uncontrolled AST recursion); existing override only covered 7.x
- djangorestframework: 3.17.1 -> 3.17.2 (DATA_UPLOAD_MAX_MEMORY_SIZE
  bypass, AdminRenderer data disclosure)

@tiptap/core is intentionally left on v2: the advisory is only patched
in 3.30.4, a breaking major upgrade that needs a dedicated migration.

(cherry picked from commit a0efb67)
* fix(security): bump the fast-uri pin to 3.1.8

fast-uri 3.1.6 is now itself affected:

  CVE-2026-84292  authority injection via unvalidated port serialization
  CVE-2026-84394  host confusion via unbalanced URI brackets, bypassing
                  security policies

Fixed in 3.1.7; 3.1.8 is the newest 3.x and carries both, so take that
rather than sitting exactly on the floor.

The pin is deliberately exact rather than a floor -- fast-uri reaches us
only through ajv, whose range is ^3.0.1, and 4.x is outside it -- so the
fix is to bump the number, never to widen the range. Rewrote the comment to
say that explicitly: 3.1.6 was itself the fix for CVE-2026-75899/-75931/
-75975/-76172, so this will recur and the next person should bump rather
than reach for a range.

Flagged on the plane-ee nightly scan (run 36652808689, 2026-09-30), which
scans the published community tags: plane-live and plane-space both
reported it, and it also accounts for the fast-uri half of the
plane-aio-community finding.

The lockfile moves fast-uri 3.1.6 -> 3.1.8 and nothing else. Verified
CI=true pnpm install --frozen-lockfile passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(deps): correct the fast-uri pin's maintenance note

Mirrors the review feedback on the enterprise counterpart
(makeplane/plane-ee#9951). Comment-only; the pin and the lockfile are
unchanged.

The reviewers there pointed out that "reaches us only through ajv" was
false, because @todesktop/cli pins fast-uri directly. That is true of
plane-ee but NOT of this repo -- checked, and there is no todesktop here at
all, so ajv@8.18.0 really is the only consumer. Said so explicitly, with a
pointer that plane-ee has the extra dependent, so the two notes do not look
like one of them is stale.

The substantive correction that does apply here: 3.1.8 was described as
merely the newest 3.x carrying 3.1.7's fixes. It is its own security
release -- CVE-2026-86472 / GHSA-hrr3-gc8f-f4qj, inconsistent host case
normalization via percent-encoded octets, patched in 3.1.8 for the 3.x
line. Presenting 3.1.7 as the safe minimum would have let a later cleanup
drop back and reintroduce it, and because that one is MEDIUM the
HIGH/CRITICAL-only nightly scan would not have caught the slip. The note now
lays out all three steps of this pin's history and says 3.1.8 is the floor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(deps): trim the fast-uri note to three lines

The maintenance note had grown to 16 lines for a one-line pin. Same facts,
stated once: exact pin not a floor, ajv is the only consumer here, and 3.1.8
is the floor because its advisory is MEDIUM and the scan would miss a slip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6aa0d88)
…ped.sh script and minio image fix (makeplane#9727)

* fix(airgapped): harden community restore script

- Fix stray "+" before set -euo pipefail
- Allow no-argument invocation under set -u (BACKUP_FOLDER="${1:-}")
- Don't exit under pipefail when no compose project matches; recognize
  "running(N)" statuses when checking if the instance is running
- Fix unquoted glob in the backup extraction loop
- Rename extracted dirs (pgdata/redisdata/uploads/rabbitmq_data) to the
  volume paths the airgapped docker-compose expects (db/redis/minio/uploads/mq)
- Update ASCII header to the current logo

Co-Authored-By: WOZCODE <contact@withwoz.com>

* fix(airgapped): address review comments on restore script

- fail loudly when compose status query fails instead of treating it as stopped
- use if ! tar guard since $? check was dead under set -e
- clean stale extracted dirs before re-extraction to avoid merged restores
- stage directory swaps with rollback so old data survives a failed move
- remove unreachable else branch with misleading error message

Co-Authored-By: WOZCODE <contact@withwoz.com>

* fix(airgapped): restore old data if swap is interrupted by a signal

Trap INT/TERM/HUP between the two mv operations in replaceDir so the
volume path is never left missing mid-swap.

Co-Authored-By: WOZCODE <contact@withwoz.com>

* chore(docker): switch MinIO image to pgsty/minio RELEASE.2026-08-04

Replace quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z with
docker.io/pgsty/minio:RELEASE.2026-08-04T00-00-00Z across the compose
files and the test docs.

Co-Authored-By: WOZCODE <contact@withwoz.com>

---------

Co-authored-by: WOZCODE <contact@withwoz.com>
(cherry picked from commit 03197e3)
…b API returns compact JSON (makeplane#9916)

install.sh and swarm.sh grep for '"tag_name": "' with a space, but the
GitHub API now returns '"tag_name":"v1.4.2"', so the lookup is empty.
Allow optional whitespace after the colon.

The exit 1 in checkLatestRelease runs inside $(...), so it only left the
subshell and install carried on with an empty APP_RELEASE ("invalid
reference format"). Exit at the call sites when the lookup fails.

Co-authored-by: WOZCODE <contact@withwoz.com>
(cherry picked from commit e6a0866)
* fix(api): pass project context when updating an intake work item

IssueSerializer.validate() scopes assignees, labels, state, parent and
estimate point to self.context["project_id"], but the API intake PATCH
built the serializer without a context. Every in-project assignee or
label submitted there was rejected with a 400.

* chore(docker): bump the turbo pin in app Dockerfiles to 2.10.11

The catalog moved to turbo 2.10.11, but the builder stages still
installed turbo@2.9.18 for `turbo prune`.

* fix(aio): stop copying node:22-alpine's /usr/lib over the runner

The copy replaced libraries just upgraded by apk (libcrypto, libssl,
libz, libapk) with node's copies from a different Alpine release. node
only links libstdc++ and libgcc_s from /usr/lib, so install those from
apk instead.

* test(api): restore shared test state precisely

- celery_eager now restores task_eager_propagates as well as
  task_always_eager
- auth tests clear only throttle_authentication_* keys instead of the
  whole shared cache; drop the duplicate module-level reset fixture

(cherry picked from commit c7a5afe)
…#9962) (makeplane#9963)

* chore(deps): switch MinIO image to pgsty/silo

PGSTY forked/renamed MinIO as Silo; pgsty/minio stays published but
frozen at its final tag. Bump the pinned image in all four compose
files to docker.io/pgsty/silo:RELEASE.2026-09-16T00-00-00Z.

Refs INFRA-1089

* fix(deps): point MinIO entrypoint scripts at the silo server binary

The pgsty/silo image dropped the legacy `minio` binary; only `silo`
(server) and `mc`/`mcli` (client) exist now. The default image
entrypoint transparently translates `server ...` args to `silo
server ...`, so services using the plain `command:` form are
unaffected. But docker-compose-test.yml and docker-compose-local.yml
override `entrypoint:` with an inline script that invoked `minio`
directly, which no longer resolves and left both services unable to
start. Point both scripts at `silo server` instead.

Also fixes the now-stale image/tag reference in
apps/api/tests/RUNNING_TESTS.md's service table.

Refs INFRA-1089

(cherry picked from commit bab49bb)

Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
@CLAassistant

CLAassistant commented Oct 9, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

pratapalakshmi and others added 22 commits October 26, 2026 17:14
…ne#10120)

Advisories that red the plane-live, plane-space and plane-aio-community
legs of the nightly scan:
- body-parser CVE-2026-111181: exact pin 1.20.6 -> 1.20.9
- undici CVE-2026-109869: exact pin 7.30.0 -> 7.30.1
- zod CVE-2023-54404: no 3.x fix, so the catalog moves ^3.25.76 -> ^4.6.7
  and an override keeps eslint-plugin-react-hooks on it too. The live
  document controller reads ZodError.issues (zod 4 dropped .errors).
- curl/libcurl CVE-2026-92392: APK_SECURITY_PATCH 2026-10-26 busts the
  cached apk-upgrade layer so a rebuild installs 8.23.0-r0.
Ports makeplane/plane-ee#9883 to the community editor and live server:
Tiptap 3.31.3, @tiptap/y-tiptap 3.0.9 and Hocuspocus 3.4.4, with no
change to stored Yjs documents or the editor schema.

- Editor: bubble menu on @tiptap/react/menus, AI menu on floating-ui
  instead of tippy.js, StarterKit's new bundled link/underline/list
  keymap/trailing node turned off, setContent on the v3 options form,
  CharacterCount/Placeholder from @tiptap/extensions.
- Document codecs: v3's @tiptap/html only works in a browser, so the
  helpers are built from one factory and a new @plane/editor/server entry
  uses the happy-dom codec; live imports from it.
- Placeholders are computed from the current view state, as in v2. v3's
  incremental placeholder state reads the previous editor state and only
  rescans touched blocks, so a comment box lost its placeholder after
  typing and deleting, and empty lines kept it after the caret left.
- Live: Redis extension on the v3 API (with Redlock on the configured DB),
  title fragment built with y-tiptap instead of @hocuspocus/transformer,
  and shutdown drains pending saves before Redis disconnects.
- Pin prosemirror-state/transform and the Tiptap menu extensions so the
  graph keeps one copy of each; dedupe linkifyjs.
- Clear the strict pre-commit lint warnings in touched files (named
  Tiptap imports, renamed shadowed variables).

Tests: placeholder regression tests in the editor, drain/Redis readiness/
shutdown unit tests and a two-server Redis integration test (v2-written
pages, cross-server sync, title migration, HTML conversion) in live.

Co-authored-by: Satyasai Vallampati <43840773+saivallampati@users.noreply.github.com>
…ect admins (makeplane#9444)

* [WEB-8291] fix: gate project invitation list/retrieve/destroy to project admins
* chore(security): drop advisory identifiers from code comments
* [WEB-8291] fix: address review feedback
…e caller (makeplane#9474)

* [WEB-8374] fix(security): scope ProjectMemberPreferenceEndpoint to the caller
* [WEB-8374] test: assert preference payload and persistence, not just 200
* chore(security): drop advisory identifiers from code comments
* [WEB-8374] fix: address review feedback
* [WEB-8374] fix: omit personal fields from member-detail responses
* [WEB-8374] fix: reject cross-member writes to all personal fields
…reator (makeplane#9483)

* [WEB-8401] fix(security): scope public Space asset mutations to the creator
* [WEB-8401] test: add unauthorized PATCH regression coverage (CodeRabbit makeplane#9483)
* chore(security): drop advisory identifiers and shorten comments
* [WEB-8401] fix: address review feedback
…ment (makeplane#9486)

* [SECUR-236] fix: harden pagination bounds and auth brute-force rate limiting
* [SECUR-236] fix: address CodeRabbit — composite account throttle key + rate guard
* chore: add copyright header to authentication test package __init__
* [SECUR-236] chore: drop overlapping per_page + auth fixes, defer to makeplane#9429 / makeplane#9335
* [SECUR-236] fix: close remaining per_page crash and view/cycle mass-assignment gaps
* [SECUR-236] fix: reject per_page=0 in get_per_page, not just negatives
* [SECUR-236] fix: address review feedback
…ace-level asset route (makeplane#9657)

* [INFRA-501] fix(security): enforce project membership on every workspace-level asset route
* [INFRA-501] fix(security): stop the external-id dedup echo disclosing foreign asset ids
* [INFRA-501] test: pin absence of the asset identifier fields on a denied dedup match
* [INFRA-501] fix(security): require project scoping on asset create/duplicate
* [INFRA-501] fix(security): validate the destination entity_id, not project_id, for PROJECT_COVER duplicates
* [INFRA-501] fix: address review feedback
…ated_at on external API (makeplane#9704)

* [INFRA-772] fix(security): stop trusting body-supplied created_by/created_at on external API
* fix: tighten created_at assertions per CodeRabbit review on PR makeplane#9704
* [INFRA-772] fix: address review feedback
…ts + activity (makeplane#9467)

* [WEB-8353] fix(security): enforce guest issue visibility on attachments + activity
* chore(security): drop advisory identifiers from code comments
* [WEB-8353] fix(security): extend guest issue visibility guard to comments + reactions
* [WEB-8353] fix: address review feedback
…esting user's own exports (makeplane#9707)

* [INFRA-778] fix(security): scope ExportIssuesEndpoint.get to the requesting user's own exports
* [INFRA-778] use status.HTTP_200_OK instead of bare 200 in the new test
* [INFRA-778] fix: address review feedback
…ed DRF mixins (makeplane#9652)

* fix(security): refuse routed actions served by unauthorized DRF mixins
* fix(security): share the non-authorizing permission set with the route scan
* fix(security): resolve perform_create against its actual owner, not just its presence
* fix(security): resolve the guard's permissions from get_permissions(), mirror partial_update on the other-surface scan
* [INFRA-496] fix: address review feedback
* [INFRA-496] test: drop routes preview now authorizes from the guard manifest

Co-authored-by: Dheeraj Kumar Ketireddy <dheeru0198@gmail.com>
…bal view (makeplane#9653)

* fix(security): require workspace membership to read a global view
* fix: 404 instead of a hollow 200 when a global view does not exist
* [INFRA-499] fix(security): reapply guest ownership restriction in WorkspaceViewViewSet.retrieve
* [INFRA-499] fix: address review feedback

Co-authored-by: Dheeraj Kumar Ketireddy <dheeru0198@gmail.com>
…o the URL project (makeplane#9706)

* [INFRA-777] fix(security): scope ProjectArchiveUnarchiveAPIEndpoint to the URL project
* [INFRA-777] fix: address review feedback

Co-authored-by: Dheeraj Kumar Ketireddy <dheeru0198@gmail.com>
…L workspace/project (makeplane#9481)

* [WEB-8400] fix(security): scope DeployBoardViewSet queryset to the URL workspace/project
* [WEB-8400] test: cover same-workspace/other-project board scope
* chore(security): drop advisory identifiers from code comments
* [WEB-8400] fix: address review feedback

Co-authored-by: Dheeraj Kumar Ketireddy <dheeru0198@gmail.com>
…nt-email-notification

fix(security): stop rendering raw comment HTML unescaped in email notifications
…reen

[GIT-395]fix: keep timeline scroll position when toggling full screen
)

* [WIKI-1268] chore: upgrade Tiptap and Hocuspocus to v3

Ports makeplane/plane-ee#9883 to the community editor and live server:
Tiptap 3.31.3, @tiptap/y-tiptap 3.0.9 and Hocuspocus 3.4.4, with no
change to stored Yjs documents or the editor schema.

- Editor: bubble menu on @tiptap/react/menus, AI menu on floating-ui
  instead of tippy.js, StarterKit's new bundled link/underline/list
  keymap/trailing node turned off, setContent on the v3 options form,
  CharacterCount/Placeholder from @tiptap/extensions.
- Document codecs: v3's @tiptap/html only works in a browser, so the
  helpers are built from one factory and a new @plane/editor/server entry
  uses the happy-dom codec; live imports from it.
- Placeholders are computed from the current view state, as in v2. v3's
  incremental placeholder state reads the previous editor state and only
  rescans touched blocks, so a comment box lost its placeholder after
  typing and deleting, and empty lines kept it after the caret left.
- Live: Redis extension on the v3 API (with Redlock on the configured DB),
  title fragment built with y-tiptap instead of @hocuspocus/transformer,
  and shutdown drains pending saves before Redis disconnects.
- Pin prosemirror-state/transform and the Tiptap menu extensions so the
  graph keeps one copy of each; dedupe linkifyjs.
- Clear the strict pre-commit lint warnings in touched files (named
  Tiptap imports, renamed shadowed variables).

Tests: placeholder regression tests in the editor, drain/Redis readiness/
shutdown unit tests and a two-server Redis integration test (v2-written
pages, cross-server sync, title migration, HTML conversion) in live.

* [WIKI-1268] fix: honor collaboration forced disconnects

---------

Co-authored-by: Sriram Veeraghanta <veeraghanta.sriram@gmail.com>
…ne#10144)

* fix(security): build caddy on go-jose v4.1.6

CVE-2026-116587 (go-jose v4, HIGH, DoS parsing RSA JWKs) is fixed in v4.1.6.
proxy pins it with --with, so the pin moves; admin and web inherit Caddy's own
v4.1.5, so they get it in the same go get pass as x/crypto and grpc.

* fix(security): bump happy-dom to 21.0.4

CVE-2026-115895 covers every 17.x-21.0.3, so there is no 20.x fix. Moving the
catalog pin also replaces the auto-installed 20.14.6 peer under @tiptap/html,
which is what plane-live and plane-space ship.

Copy link
Copy Markdown
Contributor Author

Could a maintainer approve CI for current head a13ed1c17c68cf0175652b49116b1e666baac61f? Build and lint web apps, React Doctor, CodeQL and Copy Right Check report action_required; the CLA check has passed.

The PR description includes 281 passing block tests, type/lint/format checks and a before/after browser check of the tooltip. Hosted CI results are still pending.

Thank you.

ghazy2001 and others added 3 commits November 3, 2026 00:09
…plane#10122) (makeplane#10147)

* fix(web): allow saving profile settings with an empty last name (makeplane#10122)

In Profile settings -> General, the last name field is optional, but form submission failed with 'Name is required' when left empty because validatePersonName rejected empty or whitespace-only inputs.

- Update validatePersonName in @plane/utils to accept an optional required parameter (defaulting to true)
- Update GeneralProfileSettingsForm to validate last_name with required=false so empty values pass while non-empty values enforce length and pattern constraints

Fixes makeplane#10122

* refactor(utils): extract validateOptionalPersonName to preserve validatePersonName signature
…eplane#10038)

* refactor(web): use public cycle sidebar close control

* refactor(web): use public project state fields

* refactor(web): use public Quicklinks form fields

* refactor(web): align module sidebar actions

* refactor(web): use public onboarding name field

* refactor(web): compose public auth email field

* refactor(web): use public calendar navigation controls

* refactor(web): use semantic marketing consent control

* refactor(web): name password reset visibility controls

* refactor(web): use public password form actions

* refactor(web): use public workspace name field

* refactor(web): use public analytics completion badge

* refactor(web): use public sidebar navigation rows

* refactor(web): use public sidebar project rows

* refactor(web): use public sidebar favorite rows

* chore(web): align favorite folder row controls

* chore(web): align extended sidebar navigation rows

* chore(web): align project sidebar section controls

* chore(web): use public workspace switcher identity

* chore(web): align workspace switcher trigger and header

* chore(web): align sidebar quick-action label

* chore(web): align help menu trigger and footer

* fix(web): translate extended sidebar pin controls

* chore(web): align user-menu trigger focus

* fix(web): resolve extended-sidebar accessible labels

* chore(web): translate sidebar captions and region names

* fix(web): keep workspace toggle focus ring visible

* chore(web): use semantic project drag-preview typography

* chore(web): use semantic favorite-folder preview typography

* fix(web): render favorite drag previews before capture

* fix(web): remove nested favorite-folder drag button

* fix(web): improve sidebar control accessibility

* fix(admin): make sidebar navigation accessible

* fix(ui): separate reaction controls from picker triggers

* refactor(web): use Propel inputs for header and filter searches

* refactor(web): align onboarding selections and workspace fields

* fix(web): separate page asset navigation and download controls

* fix(blocks): preserve empty states when decorative chunks fail

* refactor(web): use named Propel comment edit actions

* refactor(web): use named Propel issue and webhook actions

* fix(web): separate module info controls from navigation

* refactor(editor): use accessible copy and image upload controls

* refactor(web): associate profile field labels and errors

* refactor(web): associate project settings fields and timezone feedback

* refactor(web): associate workspace settings field labels

* refactor(web): use public password labels feedback and visibility controls

* refactor(web): associate account form feedback and public code actions

* refactor(web): compose cycle dialog sections and associated validation

* refactor(web): compose module dialog sections and associated validation

* refactor(web): associate view title validation and description

* refactor(web): compose project dialog body actions and field feedback

* refactor(web): use single estimate radio activation and named grouping

* refactor(web): use public webhook radio chrome and named grouping

* refactor(web): use a labeled public limited-project count field

* refactor(web): use public quick-add title fields

* refactor(web): make issue group disclosures keyboard accessible

* refactor(web): use public calendar month and year controls

* refactor(web): use semantic Cycle detail typography

* refactor(web): use semantic Module detail typography

* refactor(web): reuse shared Intake property rows

* refactor(web): use public export avatars and status badges

* refactor(web): use semantic error screen typography

* refactor(web): associate webhook URL labels and errors

* fix(web): match List group announcements to visible content

* refactor(admin): use semantic provider instruction typography

* refactor(admin): use semantic settings section typography

* refactor(admin): use published error banners

* refactor(admin): use semantic setup copy and telemetry link

* refactor(blocks): use semantic chart typography

* refactor(editor): use semantic link preview typography

* fix(web): translate favorite folder and drag captions

* fix(space): use an accessible auth notice

* refactor(space): align auth typography and password controls

* fix(space): align public filter controls

* fix(space): name applied filter remove controls

* fix(space): make public grouping controls keyboard accessible

* refactor(space): use semantic public board typography

* refactor(space): align comment formatting controls

* refactor(space): align comment avatars and edit actions

* refactor(space): align public peek typography and actions

* refactor(space): align vote control states

* fix(blocks): contain long selected labels

* fix(admin): bound settings content scrolling

* fix(admin): contain long copy field values

* fix(web): keep sidebar drag previews current

* fix(web): separate overflow tab preference actions

* fix(admin): expose semantic page headings

* fix(admin): show keyboard focus on scrolling content

* style(web): align shared header height

* style(web): align sidebar rows and header with shared spacing

Project rows no longer carry an extra indent for the drag handle; the
projects section extends into the left gutter instead so rows start with
the other navigation items. Section lists use 1px row gaps, the header
uses medium icon buttons, and the scroll area uses auto scrollbars.

* style(web): render board columns as panels

Board columns sit in rounded layer panels on the page surface, with the
group header, cards and quick-add inside the panel. Cards drop the resting
shadow, use the subtle-1 border and wrap long titles; group headers use
the body type scale.

* style(web): use outlined property chips on work items

List and board property chips render at the 24px outlined pill size,
sub-work-item, attachment and link counts use the shared pill chrome, and
an empty assignee chip shows only its icon (still named by its tooltip
heading).

* style(web): align list group headers and quick add row

Group headers use the body type scale, wider trailing padding and
medium ghost icon buttons for adding work items. The quick add form
renders as an inline list row with a borderless title field and the
submit hint on the right.

* style(web): align table borders, type and column icons

Table header and body cells use half-pixel subtle borders with separate
border spacing, header labels use the body type scale, and cell text
inherits the shared regular size. Column header icons now resolve, since
the icon map is keyed by the property detail icon names.

* style(web): use secondary button for calendar options

The calendar options trigger uses the shared secondary button with an
end chevron on desktop and a secondary icon button on mobile, matching
the adjacent Today control.

* style(web): use shared type tokens in the timeline

Timeline header, sidebar and week/month/quarter scales use the caption
and body type tokens in place of fixed pixel sizes, and the header row
uses the same vertical padding as other layout toolbars.

* style(web): use body type tokens in shared list rows

List rows use the regular and medium body tokens for row and title text,
tighten the actionable item gap, and accept link and title class names so
compact surfaces can adjust spacing without forking the row.

* style(web): align home widgets with the dashboard layout

The home column narrows to 750px with a heading-scale greeting and wider
gaps between widgets. Quick links render as a responsive grid of
two-line cards, recents use compact 38px rows with bare 16px icons, empty
states sit in bordered panels, and the stickies header uses the shared
secondary button. Body and caption tokens replace fixed text sizes.

* style(web): use type tokens in work item surfaces

Fixed pixel text sizes are replaced with the matching Propel type tokens (caption, body and heading scales), folding an accompanying font weight into the token. Explicit and conditional weight classes still apply on top of the token.

* style(web): use type tokens in cycles and modules

Fixed pixel text sizes are replaced with the matching Propel type tokens (caption, body and heading scales), folding an accompanying font weight into the token. Explicit and conditional weight classes still apply on top of the token.

* style(web): use type tokens in pages and stickies

Fixed pixel text sizes are replaced with the matching Propel type tokens (caption, body and heading scales), folding an accompanying font weight into the token. Explicit and conditional weight classes still apply on top of the token.

* style(web): use type tokens in project and workspace settings

Fixed pixel text sizes are replaced with the matching Propel type tokens (caption, body and heading scales), folding an accompanying font weight into the token. Explicit and conditional weight classes still apply on top of the token.

* style(web): use type tokens in navigation, onboarding and auth

Fixed pixel text sizes are replaced with the matching Propel type tokens (caption, body and heading scales), folding an accompanying font weight into the token. Explicit and conditional weight classes still apply on top of the token.

* style(web): use type tokens in shared web components

Fixed pixel text sizes are replaced with the matching Propel type tokens (caption, body and heading scales), folding an accompanying font weight into the token. Explicit and conditional weight classes still apply on top of the token.

* style(web): align pages list rows and header controls

Give list rows a 24px icon slot and control-sized access/info icons,
swap the hand-rolled search for Propel ExpandableSearch, restyle the
applied-filter groups, and use filled stars and plain buttons for the
favorite and lock controls.

* style(web): align page editor toolbar chrome

Border the sticky toolbar instead of insetting it to the content width,
size the toolbar row to the header height, mark active colors with a
ring instead of a trigger swatch, keep the outline overlay from
blocking clicks, and position the floating pane button under any banner.

* style(web): align page navigation pane and version view

Scroll the assets list in a ScrollArea with full-height thumbnails and a
compact download link, tighten info panel spacing and labels, use the
arrow close icon, and show version errors and the view-only state with
the shared empty state and Badge.

* style(web): align export page modal form rows

Use medium primary labels, keep the select chevron visible, and name
each trigger after its row.

* style(web): align stickies modal and sticky toolbar

Pin the all-stickies header above a ScrollArea in a fixed-height
dialog, use Propel buttons and ExpandableSearch for its actions, and
rebuild the sticky formatting toolbar on Propel Toolbar with the color
palette in a popover.

* style(web): align comment editor toolbars

Build the comment toolbar from Propel Toolbar groups and separators,
keep editor focus on toolbar presses, and use labelled icon buttons in
the lite toolbar.

* style(web): align top navigation and user menu layout

Group the header into start, search and action regions with responsive
gaps, use 18px header icons, drive open-trigger states from the popup
data attribute, and rebuild the user menu card with a darkened cover,
truncated name and email, and a separated sign-out row.

* style(web): restyle customize navigation dialog and project header

Use section titles, layered list boxes and Field-based checkbox rows in
the customize navigation dialog, switch the project mode options to
radio field rows with a separator, use the primary text color for the
project header name, and render the project actions trigger as an
icon button.

* style(web): restyle notifications sidebar

Replace the breadcrumb header with a titled header carrying the mark
all read and refresh actions, move filters and options next to the
tabs, render the list in a scroll area with a tab panel, emphasize
unread rows by text weight instead of a tinted background, and use
ghost icon buttons for row actions.

* style(web): align workspace switcher and favorites styling

Lay out workspace switcher rows as padded rounded cards with the tick
inside the row, scroll the workspace list in a scroll area, drop the
account email header, separate sign-out from the other actions, and
tune favorite icon sizes, star color and the new folder input.

* style(web): truncate Power K rows and refine shortcut badges

Keep command row labels on one truncating line with a tooltip for
search results, render work item identifiers outside the truncated
name, draw Mac modifier keys as icons, wrap shortcut rows, and use
workspace avatars in the workspace menu.

* style(web): tighten progress stat rows in cycle and module sidebars

Progress stat rows use body-xs text, a flexible title column with a tabular count column, 2xs avatars and compact empty states. Clickable rows are now reachable and operable from the keyboard.

* style(web): align module cards and list rows

Module cards put the info, favorite and quick-action controls in a fixed-height title row and move dates and status into a wrapping pill row. Module dates and status render as small pills with a status icon, list rows use the success ring and small ghost quick actions, and the empty lead slot uses a 20px icon box.

* style(web): align cycle list rows

Cycle rows lead with a 16px cycle status icon, open details from a compact info button that also appears on keyboard focus, and render transfer and date range as small pills. Quick actions use the small ghost trigger, and active cycle stat chips use layer hover tokens.

* style(web): restructure cycle and module detail sidebars

Detail sidebars use a larger title, a status badge or icon beside it, a Properties section built from shared property rows, and divided sections with caption section headers. Module status, dates, lead and members move into the property list, links get a header count and an add button, and the peek panels drop the unused transition.

* style(web): restyle module link rows

Link rows show the link icon in a 32px tile, the title over a muted meta line, and reveal labelled edit, copy and delete icon buttons on hover or focus. The link modal uses large fields with wider spacing.

* style(web): compact cycle and module forms, filters and transfer modal

Create and edit forms use small property pills without width caps, applied filter groups use the compact layered container with only the group label capitalised, and the transfer modal drops the title icon and uses compact option rows.

* style(web): refresh work item link, attachment and property rows

Round link cards to rounded-lg with a secondary link icon and a ghost copy
button, restyle attachment cards as single-border rows with a truncated
name, muted meta line and right-aligned actions, and give the parent,
cycle, module and label rows a consistent 32px height.

* style(web): restructure work item detail and peek layout

Show the identifier (with the parent as a breadcrumb) above a larger title,
move state, priority, assignees and dates into an inline property bar under
it, and clip long descriptions behind a Show all / Show less toggle that
expands on focus.

The sidebar and peek properties now share one component that groups the
remaining properties into collapsible Details and Project structure
sections with a created/updated footer. The detail page uses a flex-1
main column with px-8 py-6 and a px-6 py-6 sidebar; the peek view drops
the stacked spacing and lets the full-screen properties panel scroll.

* style(web): show work item activity as All / Activity / Comments tabs

Replace the activity heading with an underlined tab row (All, Activity,
Comments) that keeps the sort and filter actions on the right. Each tab
covers a fixed set of the existing activity filters, the filter menu now
refines within the active tab, and the comment box is hidden on the
Activity tab. The tabs support arrow-key navigation and the last tab is
remembered locally.

* style(web): tighten work item peek header and widget spacing

Space the peek header controls 8px apart, use an expand icon for the
full-screen link and give the layout switcher a 16px icon with a hover
background. Separate the widget action row from the widget sections with
24px, shrink the section add icons to 14px and use a small dot for the
sub-issue filter indicator.

* style(web): narrow settings content column and frame members loader

Use a 50rem content column with an inset scrollbar lane, and draw the members loader as a framed table skeleton matching the members table.

* style(web): align profile general form and overview cards

Overlay avatar, name and cover picker on the profile cover, switch the fields to InputField in a two-column grid, and give the profile overview cards a flat bordered layer surface.

* style(web): align workspace details and members settings

Use WorkspaceAvatar, TextButton and InputField in workspace details, render member avatars with Avatar, dim suspended rows, frame the empty members state, and label the delete-workspace inputs.

* style(web): align estimate settings and point rows

Render estimate points as layered cards with an actions menu, use InputField and labelled Cancel/Add buttons in point forms, and match empty-state and option card surfaces.

* style(web): align project state groups and state form

Use layered rounded group and state rows, a state-group colour swatch, a footer action bar in the state form, and a grouped skeleton loader.

* style(web): align project label rows and inline label form

Use bordered label rows with inline actions, label icons with truncating names, and an InputField-based inline form with an icon colour trigger.

* style(web): align exports, webhook form and API token list

Drop the exports header rule, frame empty states, use InputField for the webhook URL with flex section spacing, and render API tokens in a framed list with status badges and a keyboard-reachable delete button.

* style(web): align project general and members settings

Use InputField and TextAreaField in the project form, box the member defaults, and match the members header, avatar column and empty state.

* style(web): align onboarding steps and tour heading

Use selectable card styling for role, use-case and invitation choices, avatar and ghost upload button in the profile step, a compact invite row layout in the team step, and loading buttons in place of spinners.

* style(web): refine list, table, calendar and filter details

Row and header selection checkboxes share the 16px slot at the row
edge, calendar blocks sit on layer-2 pills with an icon-button menu,
quick-add forms gain consistent padding, board header actions use
ghost icon buttons and the display-filter header matches its popover
surface.

* style(web): render board swimlanes as continuous column panels

Column headers cap rounded layer panels that run through every
sub-group band at the board's 336px column width, sub-group headers
float as bordered strips across them, and the last band (or a footer
cap when it is collapsed) rounds the panels off.

* style(web): use hairline borders in timeline grid and base layouts

Timeline month, week and quarter grids draw right borders instead of
overlapping outlines and the sidebar header checkbox matches the list
slot. Base layouts get the raised selected segment in the layout
switcher and board group headers with primary titles and medium counts.

* style(web): align views, analytics and filter row details

View rows show the published state as a success badge with translated
access tooltips, applied view filters group their values, analytics
uses page gutters, h6 section titles and a stable insight-card height,
and the rich filter row drops its doubled padding.

* style(web): align intake list and status chips

Intake statuses render as propel badges, list items use the layer
hover and selected surfaces, the open-tab count uses the tab's own
styling and empty sidebars use compact empty states.

* style(web): align shared pickers, modals and loaders

The existing-work-items modal uses edge-to-edge search, selection,
list and footer sections, the cover image picker fills its popover
with stretched tabs and inline upload actions, archive tabs use
underline tabs, count chips use brand badges and the projects and
table loaders follow the current card and column geometry.

* style(web): fix work item title and detail sidebar widths

Read-only work item titles use the same heading size as the editable
title on the detail page and peek. The work item sidebar uses a fixed
320px column on desktop so the inline property bar has room, and the
module detail sidebar uses the same 16px gutter as its peek panel.

* fix(utils): keep Propel border and outline widths when merging classes

tailwind-merge read the named width steps (border-sm, border-t-xs,
outline-md, ...) as border colours, so a following colour class such as
border-subtle-1 dropped the width and outline pills rendered without a
border. The steps now extend tailwind-merge's own width groups, and the
2xs/3xs caption and 2xs body tokens are recognised as typography.

* style(blocks): let pill select triggers use the shared pill width cap

The pill and icon-pill triggers no longer clamp to 160px, so long values
truncate at the pill's own maximum width, and the select test asserts the
outline pill keeps its border width.

* style(web): align work item ids, board card actions and table cell hovers

Work item identifiers in layouts use the 12px medium caption at every
size. Board card quick actions stay in the layout while hidden so the
title no longer reflows on hover, table count and date cells use the
transparent layer hover, and the sub-work-item column's selected hover no
longer fills with the full accent colour.

* style(web): size label and draft chips and colour due dates by urgency

Label and draft property chips use the 24px pill like their siblings,
sub-work-item rows show the estimate chip, the sub-work-item count uses
its own icon, and the attachment and link counts are real buttons that
open the work item. Due dates turn crimson once they have passed and
yellow on the day itself, through a shared getIssueDueDateClassName
helper.

* style(web): highlight the filters toggle while open or filtered

The rich filters toggle uses the accent border, subtle accent fill and
accent text while the filter row is open or any filter is applied,
instead of switching to the tertiary button only when filters exist.

* style(web): size form fields and pin the project create actions

Cycle, module and page title fields use the xl InputField and the cycle,
module and view descriptions the xl TextAreaField; project create and
view forms use the lg size. Errors render through the field so labels
and messages stay associated. The project identifier info icon becomes a
focusable end-of-field icon button with the same tooltip. The project
create modal insets its content, keeps the cover rounded inside it, lets
the fields scroll under the cover and pins the action row.

* style(web): use compact search empty states for no-result lists

Cycles, archived cycles and views lists show the compact search empty
state when filters or search hide every item, and archived modules the
detailed search state, instead of the legacy filter illustrations. The
archived lists reuse the existing translated copy rather than hardcoded
English, and the unreachable "No results found" branch in the views
list is removed.

* style(web): use underline route tabs and an always-visible table search in analytics

Analytics tabs are underline links to their routes, filling the page
under a header aligned to the page gutter. The insight table always shows
a large search field next to its row count, exports through a medium
secondary button with the export icon, scrolls inside a bounded frame
under its sticky header, and shows the search empty state when a search
matches nothing. Chart and skeleton heights come from one shared class.
The no-projects action label is translated.

* style(web): show the work item analytics button as an icon in narrow headers

Below the header's @4xl container breakpoint the analytics action is a
secondary icon button with the analytics icon and its tooltip; at and
above it, a labelled secondary button.

* style(web): use detailed empty states for 404, error and not-found screens

Swap the legacy 404, server error, not-authorized, workspace-not-found,
profile no-access and work item/cycle/module/view not-found images for
the shared empty-state illustrations at size-40 with lg actions. The
production error page gains a Reload action next to Go to home.

* style(web): move auth forms onto xl input fields

Email, password, unique code, forgot, reset and set password forms use
the ready-made xl input field with labels, end-icon toggles and inline
errors. The forgot password link grows to caption-md and, without SMTP,
reveals an inline danger banner instead of a popover. Resend code is a
plain text button, the invite header shows an xl workspace avatar and
auth error banners use the info variant.

* style(web): refresh invitation and create workspace screens

Workspace invitation states and the empty pending-invites page use the
shared empty-state illustrations with real buttons and links in place
of the old list rows. Pending invites show an xl workspace avatar and a
capitalised role under an h6 lead-in. The create workspace form caps at
24rem and the creation-disabled copy uses body-xs-medium.

* style(web): use shared empty states on the stickies page

The stickies page empty and no-results states use the stickies and
compact search illustrations, and the header icon drops to size-4 in the
tertiary tone without the rotation.

* feat(web): describe the empty notification detail pane

Show the inbox illustration with a title and description when no
notification is selected. Add notification.empty_state.detail.description
and issue.empty_state.no_access.{title,description} in every locale, and
retitle the notification detail empty state. Translations are machine
generated and need native review.

* feat(blocks): add entity detail, activity timeline and comment box blocks

entity-detail provides the bordered widget toolbar (a flat medium Propel
Toolbar with separated groups and text, menu and icon buttons) and the
collapsible widget section with title, count, caret and open-only
actions. activity provides the timeline row (bordered icon box on a rail,
actor, message, dot and time) and the comment card chrome with a 16px
avatar column. comment-box wraps an editor with a formatting toolbar
slot and a submit button.

* feat(i18n): add work item detail toolbar, archived banner and updated strings

Adds the accessible name for the work item widget toolbar, the archived
work item banner message and the "Updated {time}" sidebar meta in every
locale.

* style(web): group work item widget actions in one toolbar

The add sub-work item, relation, link and attach actions sit in one
bordered toolbar with separated groups, keeping their menus, file picker
and drop target. Sub-work items, relations, links and attachments become
divided sections with a plain title, count and caret, actions only while
open and no body inset. The sub-work item progress is a compact ring and
x/y chip beside the title, and relation groups are headed by soft pills.

* style(web): use timeline rows, comment cards and a comment box in activity

Activity rows use the timeline block: a raised bordered icon box on the
rail, 24px row spacing, medium actor, secondary message and a dot before
the time. Comments render as raised cards with a 16px avatar column,
author, "commented", visibility and time on one line. The composer sits
in a bordered comment box with the formatting toolbar and a Comment
button in its footer; mentions, uploads and Enter to submit are
unchanged. Empty activity and comment tabs show a compact empty state.

* style(web): polish work item sidebar, peek archive state and create modal

The detail sidebar shows "Updated <time ago>" beside the Properties
title and uses the created, updated and completed date icons for its
audit rows. The subscribe button is label only. An archived work item
in the peek shows a warning banner with Restore and dims its content.
The create modal description box gains hover and focus states, and the
selected parent is a bordered pill with a labelled remove button.

* style(web): turn the display and work item filter dropdowns into menus

The shared filters dropdown opens a Propel menu instead of a popover.
Display shows the property toggle pills on top, then Group by, Sub-group
by and Order by as submenus that name the current value, with the
direction as its own choice under Order by, and the extra options as
checkbox rows. Work item filter sections become one submenu per property
with a search on long lists, and date filters open the custom range
modal from a host that outlives the menu.

* style(web): move project, cycle, module, view, page and intake filters into menus

Each filter dropdown now lists one submenu per property with the
applied count at the end of its row, a search inside long lists such as
members and labels, boolean filters like My projects and Favorites as
checkbox rows above a separator, and date filters whose Custom row opens
the range modal. The module filters drop their bespoke menu wiring and
use the shared filters menu.

* style(web): show applied filter values as removable chips

Adds a RemovableChip on the Propel combobox chip, with an outline look
and a soft look for chips inside a filter group, and uses it for every
applied filter value on the projects, cycles, modules, views, intake and
work item rows. Each filter group now sits in the same bordered layer-2
frame with the property name, its chips and the clear control.

* style(web): render work item filter conditions as pill groups

A filter condition is now one Propel pill group: the property as a pill
with its icon in the tertiary tint, the operator and value as 28px pill
select triggers, and remove as an icon pill. An invalid condition
outlines every segment in danger. The add-filter dropdown can take the
property pill as its trigger so the group keeps it as a direct segment.

* style(web): use the expandable search in list headers

The cycles, archived cycles, modules, archived modules, views and
projects headers swap their hand-built search toggle for the Propel
expandable search with a clear control. The modules header now reads
search, layout, an icon-only Filters button and Order by.

* feat(utils): add an off-DOM measureTextWidth helper

Measures text in a CSS font on a shared canvas (cached per font and text) so
layouts can estimate chip widths without forcing a reflow, with an
average-glyph fallback where no canvas exists (SSR, jsdom).

* feat(blocks): add LabelPill and show label colours as Propel swatches

LabelPill is a display-only 24px Propel pill (soft or outline) with the
label colour as a Swatch and the name as a truncating PillLabel, capped at
the smaller of 224px and its container. The label select lists selected
labels as soft label pills in its field variants, uses swatches instead
of hand-drawn dots in its trigger and options, and can hide the empty
label icon with showEmptyIcon.

* feat(blocks): add a single-line work item properties row with a +N menu

PropertiesOverflowRow fits a work item's property chips into a width
budget, most important first, and collapses the rest into a +N pill whose
Properties menu lists each hidden property with its value; editable rows
open the property's own editor, count rows run their action and
read-only rows are disabled. Shrinkable chips (labels) step down before
anything collapses, and a chip whose dropdown is open is never collapsed.

* feat(i18n): add strings for the list row properties overflow

Adds issue.properties_overflow.trigger_aria_label (the +N trigger's
accessible name) and issue.properties_overflow.label_overflow_indicator
(the +N labels pill) in every locale.

* style(web): keep list row properties on one line with a +N menu

From the single-line breakpoint, list rows keep their property chips on
one line within the room left after a 240px title floor; chips that do
not fit collapse into a +N pill opening a Properties menu where each
property can still be edited. Labels step down from up to three outlined
label pills, to fewer pills with a +N labels pill, to the N labels chip.
Narrow screens keep the wrapping row and board cards are unchanged.

* style(web): collapse workspace draft properties into a +N menu

Workspace draft rows use the same single-line properties row as work item
lists, including the labels step-down.

* style(web): show work item sidebar labels as soft label pills

The labels property in the work item sidebar, peek and intake lists the
selected labels as soft label pills with colour swatches and no longer
shows the label icon while empty.

* style(web): label multiple assignees as a count in the property bar

The member select accepts a multipleLabel for selections of more than
one member, and the work item property bar uses it to show "N assignees"
instead of hiding the label. Adds issue.assignee_count to every locale
(machine-translated plural forms; needs native review).

* feat(web): add a sticky column picker column to the spreadsheet layout

The table ends in a narrow column pinned to the right edge. Its header holds a
column picker that toggles the built-in display properties through the same
filter store update as the Display menu, and each row's quick actions move out
of the title cell into it, revealed on row hover, keyboard focus or while the
menu is open. The loaders draw the extra column too.

* feat(blocks): add activity, archive, comment and widget empty-state illustrations

Draws new card-stack illustrations for activity timelines, archives,
comments and widgets (plus a plate-stack archive for detailed empty
states) using the shared illustration colour tokens, so they follow
light and dark themes. The art is decorative and hidden from assistive
technology. Registers the keys so EmptyStateCompact/EmptyStateDetailed
resolve them, with render tests for each key.

* style(web): show the activity illustration in profile activity empty states

The profile overview's recent activity card and the first page of the
profile activity list now show the activity illustration with the
"No activity yet" message instead of the generic fallback or an empty
list. Comment actors in the activity list use the shared avatar at the
medium size instead of a hand-rolled image.

* style(web): use block illustrations for home widgets and work item activity

The home page's no-widgets state uses the compact empty state with the
widget illustration instead of a theme-switched raster image, and the
work item activity and comments tabs use the activity and comment
illustrations instead of the update and note fallbacks.

* style(admin): rebuild the sidebar on the shared list and overlay pattern

Render the instance settings menu as a comfortable list inside a scroll area, showing
names only with descriptions in tooltips. The sidebar is 288px wide (70px collapsed),
overlays the page below md with a backdrop and Escape to close, and the layout fills
the dynamic viewport. The header uses the user-cog tile and a round avatar with a
danger sign-out row, and the footer shows a secondary Redirect to Plane button.

* style(admin): align page header spacing, copy fields and setting switches

Pages start 24px (32px on md) from the header with the title row stacking on small
screens, and wide pages keep a 16px inset. Copy fields show the value in a bordered
code box with a ghost copy button, toast on success or failure, and switch labels
read as primary body text.

Adds common.clipboard_copy_failed, common.tooltip.copy_to_clipboard and
instance_workspaces.empty_state in every locale.

* style(admin): align authentication cards, provider forms and email settings

Authentication method cards stack on small screens with a rounded icon tile and a
larger title and description, and Configure uses the preferences icon. The sign-up
toggle sits in a bordered card. Provider forms use tighter column gaps and primary
section titles with the Plane-provided details in a panel below their heading. Email
settings label the security select as a field and the test email dialog is wider
with a labelled input.

* style(admin): align workspace, setup and not-found screens

The workspace form shows validation as field errors with translated labels and a
secondary organization size select, and the workspace list gains a bordered empty
state, a stacking header and a bordered creation toggle card. The 404 page uses the
shared detailed empty state. Instance setup uses xl inputs, the shared password
strength meter and an inline danger banner, which sign-in also uses, so the local
banner and meter copies are removed.

* style(admin): use shared skeletons, lockup and type tokens

Loading placeholders and the Plane lockup now come from the shared blocks package,
so the local copies are removed, and the last fixed text sizes in the new user popup
and instance failure screen move to body and heading type tokens.

* style(space): lay the public board out as column panels on surface-1

Board sits on surface-1; kanban columns become rounded layer-1 panels
with the header inside, list group headers get a layer-1 bar with a
body-sm-medium title, and an empty board shows the compact work item
empty state.

* style(space): give board cards the raised card shell

Cards use a subtle-1 border on layer-2 that lifts on hover, pad the
body at 12px with 12px gaps, wrap long titles and put the reactions
footer on layer-2.

* style(space): show board properties as read-only outlined pills

State, priority, labels, dates, assignees, modules and cycle render
through the shared property selects in disabled mode as 24px outlined
pills (clicks fall through to the card), counts become outlined Pill
chips, due dates take the shared overdue/due-soon tone, and the board
honours the default computed display properties.

* style(space): align the peek properties and comment thread

Peek properties sit in fixed-width label rows (16px icons, 32px rows)
with read-only ghost selects and a new Labels row. Comments open with
the composer, list newest first and use the shared activity comment
card with the timeline rail.

* style(space): replace legacy text sizes with type tokens

Fixed pixel text sizes move to the Propel type scale with weights folded
in, and the unused label filter components and renderEmoji helper are
removed.

* style(space): use the shared empty states for error and 404 pages

The not-found pages and the board error state render EmptyStateDetailed
with the 404 and server-error illustrations, replacing the local SVGs.

* style(space): build the comment toolbar on the Propel Toolbar

Formatting controls become grouped Toolbar toggles with separators and
tooltips, keep editor focus on press, and the submit button sits beside
the toolbar instead of inside a bordered strip.

* style(space): polish the board filter menu

Options use the Propel checkbox and radio, section headers read as
placeholder text on a layer-2 sticky bar, the search field drops its
extra side padding and the Filters trigger moves to the md size.

* style(space): match the navbar layout switcher and sign-in button

The layout switcher uses the raised segmented buttons with tooltips and
the sign-in button moves to the md size.

* feat(blocks): add SegmentedProgress and ProgressRing progress blocks

SegmentedProgress draws a stacked bar on a surface-2 track with one
coloured segment per item sized by its share of the total, a tooltip per
segment, four sizes and optional click/keyboard activation. ProgressRing
is a sized circular ring with a strong-border track, a success arc and a
centred slot for a percentage or check mark.

* style(web): show module and active cycle progress as state group bars

Module cards and the active cycle progress panel replace the single-value
linear bar with a large segmented bar (one segment per state group, with
a tooltip each). Module list rows draw their completion with a 30px
progress ring with a 3px stroke instead of a scaled circular progress.

* feat(i18n): add module progress summary and board collapse strings

module.details_drawer.progress.{done,points_done,empty} back the module
sidebar progress headline and empty state; aria_labels.project_board.
collapse_group names the board column collapse button. All locales.

* style(web): redesign the module details sidebar

The header gains the module quick actions beside close. A long
description clamps to two lines with show all / show less. Properties,
progress and links are collapsible groups separated by dividers. Progress
leads with a bold percentage and a "N of M work items done" caption
(points when plotted by points), a thin stacked state bar and a
two-column state group grid with count and share; on the module page the
tiles filter the work items by state group, replacing the old States tab.
The burndown chart and the assignee and label breakdown stay below.

* style(web): clip long cycle sidebar descriptions behind show all

The cycle sidebar description renders at the xl text size inside the
work item description overflow, so anything taller than 140px fades out
behind a Show all / Show less toggle.

* style(web): make collapsed board columns one expand button

A collapsed column is now a single button over the whole strip with a
layer hover, focus ring and a "name · count" tooltip that follows the
cursor. It shows the 16px group icon, the vertical name capped at
max-h-100, the count and a 12px expand glyph. Columns can also be
collapsed in swimlanes: the header cell, every band cell and the footer
cap become expand targets that highlight together on hover. The
expanded header's collapse button gets a translated label.

* feat(blocks): let FavoriteStar take a button size and variant

Callers can now ask for a bordered secondary star or a larger box; the
compact ghost button stays the default.

* style(web): redesign the project card around a faded cover and footer

Shorter cover fading into the card, logo tile in the corner, actions and
favorite revealed on hover as secondary buttons, name and identifier below
the cover, a lead row, and a footer with a Joined/Archived badge or Join
button next to the member stack. Settings, restore and delete move into the
card menu. The loader mirrors the new card.

* style(web): build settings sidebars from list rows with section headings

Workspace, project and profile settings sidebars now use comfortable list
rows with medium icons under static section headings, with arrow-key
movement inside each group. The workspace and project headers stay pinned
above the scrolling list with a tertiary back button and a large workspace
avatar. The profile settings modal gets a wider surface-1 sidebar, roomier
content padding and a ghost close button.

* feat(blocks): add a type-to-confirm dialog for destructive actions

An alert dialog with a danger badge, corner close and one or more labelled
inputs that must match before the danger confirm unlocks. Enter in any
input confirms, the typed text clears each time it opens, and close
requests are ignored while the action runs.

* style(web): confirm project and workspace removal by typing in an alert

Delete project, leave project and delete workspace move to the shared
type-to-confirm alert and keep their name plus phrase inputs. Leaving a
workspace now also asks for the workspace name.

* style(web): give state rows a persistent actions menu and default badge

Each state row now shows a larger state icon, a medium-weight title, a
Default badge on the default state and an always-visible menu with edit,
mark as default and delete (disabled with the reason when the state can't
be removed, confirmed before deleting). The drag handle is a bordered chip.

* style(web): add a sticky search and add toolbar to project labels

The labels page heading drops its button; a sticky toolbar below it holds a
search field and the Add label button. Searching flattens the tree into
the labels whose name matches.

* feat(constants): add help-center links for settings pages

A map of docs.plane.so pages for the workspace and project settings screens that show a Docs link.

* style(web): link settings headings to their docs and enrich boxed controls

Settings headings take a docHref and show a small secondary Docs link after
the description; states, labels, feature, exports and webhooks pages use
it. Boxed control rows can now carry a leading icon tile, an accessory
beside the title, a docs link and an attached footer panel.

* style(web): present project features as a titled section of enable toggles

The features list gets a smaller section heading and each row reads
"Enable <feature>" with the Pro badge beside the title.

* style(web): show automation options in a panel attached to each toggle

Auto-archive and auto-close become boxed toggle rows with a medium switch;
while enabled, their duration and state options slide open in a panel
joined to the bottom of the row.

* style(web): list access tokens in a searchable table

Personal access tokens move from stacked cards to a table with name (and a
description tooltip), status badge, created, expiry and last-used dates
and a row menu for deleting. A toolbar above it filters by name or
description and holds the add button once tokens exist.

* style(web): give workspace members a described heading and toolbar row

The members page uses the settings heading with a short description, and
search (now an expandable search), role filters and Add member sit in a
right-aligned toolbar row with a divider beneath it.

* style(web): start workspace exports from a heading button and dialog

The inline export form moves into a medium dialog opened by an Export
button in the page heading, with project and format pickers on label
rows and Cancel/Export actions; the page below lists previous exports.
The dialog closes once an export is queued.

* style(web): list webhooks in a table with a row actions menu

Webhooks show as table rows with the URL (linking to the webhook), an
Active/Disabled badge and the created date. The inline switch moves into
a row menu with edit, enable/disable and a confirmed delete; the delete
modal now also takes the webhook id directly.

* style(web): use an expandable search in the project members toolbar

The always-open search box beside the role filter becomes a collapsed
search icon that expands into the input, like the workspace members page.

* chore(web): remove assets and helpers left unused by the UI refresh

Deletes empty-state, auth and filter illustrations that the shared empty
states replaced, the EmptySpace component, the old FilterHeader and
FilterOption popover helpers, and the page lock text and icon
animations that no component uses any more.

* feat(web): show a no-access state for forbidden work item links

The work item identifier lookup keeps the HTTP status on its thrown
payload, and the browse page shows the no-access empty state when the
API answers 403 instead of the generic not-found screen.

* style(web): use the archive illustration for archived projects and link the estimates docs

The archived projects empty state shows the archive illustration instead of
the archived work item one, and the estimates settings heading links to its docs.

* fix(web): show the estimate value in the properties overflow menu

A set estimate rendered an empty preview in the "+N" properties menu; it now
shows the point's display value like the other rows.

* style(web): keep focus rings visible on the edge chips of a list row

The clipped properties row gets 2px of padding offset by a matching negative
margin, so the first and last chips' focus rings are no longer cut off.

* feat(blocks): add entity detail votes and voters modal

EntityDetailVotes renders round up/down vote toggles, tinted while the
viewer's vote is on them, each followed by a count that opens the voter
list. EntityDetailVotesModal lists voters with avatars under upvote and
downvote tabs, with an empty state per tab.

* style(space): show votes with a voter list modal

Board card footers and the peek view use the shared votes block: round
vote toggles, and clicking a count opens a modal listing who voted on
each side. Adds issue.vote title, tab count and empty-state strings.

* style(admin): build the header on the shared breadcrumbs

The admin header is a header landmark with px-4 padding, its trail uses
the shared Breadcrumbs (collapsing on narrow screens with base-path
aware links), and the mobile sidebar toggle is a ghost icon button that
reports its expanded state.

* fix(admin): enable Save only for unsaved changes

General, image and email settings keep Save disabled until a field
changes and reset to the saved values after a successful save. Picking
an email security option now counts as a change.

* style(web): show loading and retry states for the work item description

The description area shows a two-line skeleton while the work item details load, and a "Couldn't load the description" line with a Retry button when they never arrived, instead of mounting an empty editor.

* style(web): fall back to the link glyph when a link favicon fails

Link rows keep the favicon from the link metadata, but swap back to the link glyph when the image fails to load, and the favicon is now decorative (empty alt).

* style(web): enlarge activity sort and filter buttons and mark applied filters

The activity header sort and filter buttons use the md icon button size, and an applied filter shows the filled filter glyph instead of the corner dot.

* style(web): break down sub-work item progress in a hover popover

Hovering the sub-work items progress chip opens a 26rem popover with the completion percentage, a segmented bar per state group and the five state group counts.

* style(web): edit comments inside the comment box chrome

Editing a comment uses the same bordered comment box as the composer: formatting toolbar and a ghost Cancel button in the footer, Save as the submit button. Mentions, uploads and Enter-to-save are unchanged.

* style(web): translate the formatting toolbar and intake tooltip labels

The comment formatting toolbar's accessible name and the intake list item's created-on and priority tooltips now come from translations.

* style(web): stack the snooze dialog pickers with AM/PM tabs

The custom snooze dialog now stacks the date and time pickers, switches AM/PM with contained tabs in the time picker header and translates its title, labels, placeholders, empty message and actions (new notification.snooze.modal.* keys and a common submitting key in every locale).

* style(web): tidy the inbox display menu and load more link

The inbox display options open from a preferences icon with a translated label, their checkbox rows drop the leading icons, and loading the next page is a plain accent text link instead of a ghost button.

* style(web): align home recents rows and widget loaders

Recent work item state and priority glyphs sit in medium icon slots with plain tooltips, assignees and project members show as a compact avatar stack of up to three (ButtonAvatars gains a max option), the recents filter pill uses medium body text with a small chevron, and the recents and quick links loaders mirror the real rows and link grid.

* style(web): combine the invite email and role into one control

Each onboarding invite row is a single bordered control holding a borderless email input, a divider and a ghost role select, with labelled inputs, translated column headings and an always visible remove button that is disabled for the last row.

* style(web): scroll the extended projects sidebar in an inset area

The expanded projects panel lists projects in an inset scroll area with even padding and tight row spacing, uses the compact search illustration when nothing matches, translates its title and casts a directional shadow.

* style(web): point the help menu sales item at the sales page

Contact sales uses the alternate user icon and opens the public talk to sales page.

* feat(i18n): add page version history, locked badge and view control strings

Adds the version pane's view-only, loading, error, retry and restore strings,
a Locked label for saved views, and the Save menu / hide-filters labels for
the filters row, in every locale.

* style(web): size the page title area to fit the page icon

The space above the page title is 64px, or 71px once a page icon is set,
and the icon tile sits directly under it without the extra top margin.

* style(web): give page version history its own pane with day groups

Version history moves out of the Info tab into its own navigation pane view,
opened from the page options menu, with a title row and close button. Saved
versions are grouped by day (Today, Yesterday, then dates) under small
caption labels, the current version leads today's group, and a failed list
load shows a retry. The version preview header gains a top border and its
strings are translated.

* style(web): show work item count and status badges in view headers

Saved view headers show the work item count as a small brand badge next to
the view name and a neutral Locked badge with a lock icon when the view is
locked. Project views also show a green Live badge linking to the published
view, and the Display menu opens from an icon-only preferences button.

* style(web): merge filter row save actions and add a minimise button

When a view can be both updated and saved as new, the filters row shows a
single Save menu holding Update view and Save as instead of two buttons;
with only one available it stays a plain button. A ghost collapse button
at the end of the header row hides the filters row again.

* style(web): align timeline and list quick-add rows with their layouts

The timeline full-screen toggle uses the expand arrows icon. The timeline
quick-add row is 48px tall with 16px side padding and a large title input.
The list quick-add identifier takes the same width as row identifiers and
reserves the sub-work-item chevron's slot, so the title input lines up with
the row titles above it.

* style(web): show calendar work item ids with a truncating project key

Calendar blocks render the work item id in the medium caption style, with
the project key truncating ahead of the sequence number so the number stays
readable in narrow day cells (the full id stays available as a title and to
screen readers). The hover preview uses the rich popover style explicitly.

* fix(utils): parse the font size without a backtracking regex

measureTextWidth's fallback read the font size with /(\d+(?:\.\d+)?)px/, which
CodeQL flags as polynomial on long digit runs. Scan the shorthand's tokens
instead; output is unchanged.

* fix(blocks): keep overflow row and type-to-confirm state out of effects

- use-properties-overflow: update the latest-inputs ref in a layout effect
  instead of during render, and disconnect the row's MutationObserver through
  the ref callback's cleanup.
- PropertiesOverflowRow: close the +N menu while rendering when it is forced
  shut or nothing is collapsed, instead of a frame later in an effect.
- TypeToConfirmDialog: clear the typed values once the close animation
  completes, instead of in an effect when it reopens.

* fix(web): leave cancelled items out of the module list progress ring

The list ring counted cancelled work items as done, so it disagreed with the
module sidebar, which now measures completed / (total - cancelled). Use the
same rule on the list.

* fix(web): show an error when enabling or disabling a webhook fails

The row menu's toggle discarded the rejected update, so the badge stayed as it
was with no feedback. Catch it and show the existing "Webhook not updated" toast.

* fix(web): unlock destructive confirms on the name only and translate them

Delete project, leave project and delete workspace no longer require typing an
English passphrase ("delete my project", "Leave Project", "delete my
workspace"); typing the project or workspace name unlocks the action, as
leave-workspace already did. Titles, descriptions, labels and toasts in these
dialogs now go through t(), with new keys in every locale.

Leaving a project also navigates to the projects list only after the API call
succeeds, so a failed leave keeps the dialog open instead of moving the user
away while they are still a member.

* feat(blocks): add a wrapping work item properties list and a count chip

WorkItemPropertiesList renders property entries in row order inside a wrapping
container, the no-budget counterpart to PropertiesOverflowRow without its
observers. WorkItemPropertyCountChip is the outlined sub-work item, attachment
and link count chip: a button with onClick, or a display-only span without.

* refactor(space): render public property chips through the blocks properties list

Space's IssueProperties now builds the same keyed property entries as the web
app and renders them with WorkItemPropertiesList, instead of a copied tree of
per-property wrappers. Count chips use the shared WorkItemPropertyCountChip;
web's board fallback and count chips use the same pieces. Chip order, gating
and appearance are unchanged.

---------

Co-authored-by: Sriram Veeraghanta <veeraghanta.sriram@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature]: Decrease ideal work item precision