Views broken by the bug above keep their state until something writes
them again, so repair them on startup: set the manual mode, seed the
default buckets when the view has none and place the project's tasks in
the default bucket.
Views whose project or saved filter is gone are skipped rather than
repaired with a dangling creator, soft-deleted tasks stay out of the
backfill, and the mode flip is the last write per view so an
interrupted run picks the view up again.
createTask becomes a wrapper around createTasks, which creates a whole
batch in one pass: project, creator, views and default buckets are
looked up once, indexes are assigned from a single max-index query
(preset indexes kept when free, collisions get the next free one), and
explicitly provided buckets are resolved once per distinct bucket —
verified to belong to the target project and checked against their
limit with the batch's own members counted. The row insert stays per
task because multi-row inserts don't reliably return autoincrement ids
on all supported databases.
Validation errors always carry the payload index; the single-create
wrapper unwraps them so existing callers keep their raw error types.
A new TasksBatchCreatedEvent fires once per batch (single create is a
batch of one) for listeners which can process all new tasks in one
pass; per-task TaskCreatedEvent semantics stay unchanged for webhooks,
mentions and audit.
Split for reviewability: builds together with the follow-up commit
adding the BulkTaskCreation model and error types.
349e6a590 renamed ListCreatedNotification's name from list.created to
project.created without rewriting the notifications.name column, so instances
upgraded from 0.19 or earlier still hold list.created rows carrying a full
project payload. Nothing can hydrate them, so nothing can scope them either,
and the read paths pass unrecognised names through unfiltered — leaking the
project's title, description, identifier, colour and owner to users since
removed from it.
Retaining rows no type can render only preserves that leak, so they go.
Notification rows outlived access. A subscription survives a project being
unshared, so every notification already written for a revoked user stayed
readable — comment bodies, task titles, project names, deletion notices. The
read paths filtered on notifiable_id alone, with no permission check anywhere.
#3325 stopped the sender writing new ones; this is the other half.
The project a notification is about is persisted on the row when it is written
and the read paths filter on it in SQL, so LIMIT, OFFSET and total are all
computed on the filtered set. Notification types declare their project through
a capability interface in pkg/notifications, the same way they already declare
SubjectID, ThreadID and ToTitle — which is what lets the package below
pkg/models stay ignorant of what a project is.
project_id 0 means account-scoped and always visible, a positive value is
checked against the projects the caller can read, and -1 marks a project-scoped
row whose project could not be determined, so it is visible to nobody. Filtering
reuses the existing accessibleProjectIDsSubquery, so the page query and the
count cannot drift apart. A migration backfills existing rows from their stored
payloads, resolving through soft-deleted tasks so task.deleted rows still land
on their project.
Covers every read path: the v1 and v2 list endpoints, mark-as-read (which
echoes the payload back), the Atom feed, and the websocket push — the last of
which is load-bearing, since a row is still written for a revoked subscriber.
Deliberately no instance-admin bypass: notifications are always the caller's
own, and being an admin says nothing about whether they should still read a
comment out of a project they were removed from.
Recreates model-declared indexes that are missing, matching by column
set so converted databases with differently-named equivalent indexes
are left alone. Fails with an actionable message if duplicate values
block recreating a unique index.
Fixes#3244
xorm's Sync drops every index and unique constraint the synced struct
doesn't declare (#3244). Adds a partialSync helper (no drops) and
converts all 65 existing-table struct syncs to it — the four v2.4.0
migrations that wiped users/tasks indexes on every upgrade, plus every
historical one back to 2019, each a latent index wipe and a pgloader
2BP01 abort point for installs upgrading from versions predating it.
Every task creation looks for rows with the same position in the view to
resolve conflicts. Without an index on position that scans all positions of
the view, so creating n tasks in one project costs O(n²) row reads - the
second half of the slow import in #3297.
A pair returned by the GroupBy was just reported as duplicated, so a row
must exist. Continuing on !has would let the delete loop drop every row
for that pair without re-inserting one, silently losing positions. Abort
the migration instead.
A task could end up with more than one task_positions row for the same
(task_id, project_view_id): rapid/concurrent creation raced the
check-then-insert paths, and the create path could insert a position that
a triggered RecalculateTaskPositions had already persisted for the new
task. The table had no unique constraint, so the duplicates were stored
silently (#2844).
In the table view this made the LEFT JOIN on task_positions emit the task
twice; getTasksForProjects enriched only the map entry, so the duplicate
slice row kept an empty identifier and rendered as "#N" instead of
"PREFIX-N" (#2725).
- Add a unique index on task_positions(task_id, project_view_id) via a
dedup migration (mirrors the task_buckets fix in 20250624092830) plus the
unique(task_view) struct tag so fresh installs get it too.
- Harden the create path: only queue a position insert when one does not
already exist for the task+view, and dedupe within the batch.
- Dedupe the task slice returned by getTasksForProjects by id, returning
the enriched entry, so duplicate position rows can never surface a task
twice or with a missing identifier.
Fixes#2844Fixes#2725
Implement the license validation system with:
- Server communication with retry logic and exponential backoff
- In-memory state management for feature flags and user limits
- Cached validation with 72h expiry stored in database
- Background goroutine with adaptive check intervals (24h/1h)
- Graceful degradation to community mode on failure
- Instance ID generation and persistence
Add the OAuthCode model for storing short-lived authorization codes
with PKCE challenges. Codes are hashed (SHA-256) before storage and
are single-use with a 10-minute expiry. Add the database migration
and OAuth-specific error types.
MySQL does not support CREATE INDEX IF NOT EXISTS syntax. Switch on
database type to use IF NOT EXISTS for Postgres/SQLite and plain
CREATE INDEX with duplicate key error suppression for MySQL.
Fixes#2431
Add user_id column to webhooks table (nullable, for user-level webhooks
vs project-level). Extend webhook model, permissions, and listener to
support user-level webhooks that fire for user-directed events like
task reminders and overdue task notifications.
Add TasksOverdueEvent for dispatching overdue notifications via webhooks.
Update webhook permissions to handle both user-level and project-level
ownership. Add webhook test fixture and register webhooks table in test
fixture loader.
Typesense was an optional external search backend. This commit fully
removes the integration, leaving the database searcher as the only
search implementation.
Changes:
- Delete pkg/models/typesense.go (core integration)
- Delete pkg/cmd/index.go (CLI command for indexing)
- Simplify task search to always use database searcher
- Remove Typesense event listeners for task sync
- Remove TypesenseSync model registration
- Remove Typesense config keys and defaults
- Remove Typesense doctor health check
- Remove Typesense initialization from startup
- Clean up benchmark test
- Add migration to drop typesense_sync table
- Remove golangci-lint suppression for typesense.go
- Remove typesense-go dependency
Adds the `sessions` table for storing server-side session records.
Each row tracks a session ID (UUID), user ID, hashed refresh token,
device info, IP address, and timestamps.
Avoid using the generic renameColumn helper for this migration on MySQL because it renames columns as BIGINT. Handle the teams oidc_id -> external_id rename with a MySQL-specific CHANGE statement that keeps VARCHAR(250) and remains idempotent.
The new catchup migration compared the postgres JSON column with string literals using !=, which fails in migration smoke tests. Use a PostgreSQL-specific WHERE clause with ::text and cover it with unit tests.
Convert bucket_configuration filters regardless of bucket_configuration_mode and catch remaining view-level filter values still stored in the legacy string format. Includes focused tests for the bucket conversion helper logic.
Fixes#2172Fixes#2188Fixes#2202
Use the shared renameColumn helper for non-SQLite databases and skip the SQLite table rebuild when oidc_id is already absent. This prevents failures after partial upgrades where the column was already renamed.
Refs #2172
Refs #2285
tx.Sync() fails on PostgreSQL because it tries to reconcile primary key constraints/indexes, causing index-drop errors. Use explicit ALTER TABLE ADD COLUMN with existence checks instead.
Fixes#2215
This change modifies the migration `20251001113831` to flexibly parse bucket configuration filters.
This fixes this migration issue:
```
json: cannot unmarshal object into Go struct field bucketConfigurationCatchup.filter of type string
```
This occurred when a single `bucket_configuration` JSON array contained
mixed formats - some buckets with old string filters and some with
already-converted object filters.