Skip to content

Commit 3508393

Browse files
committed
Add paged item link activity and Bun test tooling
1 parent 6b499ba commit 3508393

21 files changed

Lines changed: 1566 additions & 135 deletions

.bun-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.4.0

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ blog/
185185
!/docs/recurrence-scheduling-safeguards.md
186186
!/docs/jira-service-management-import.md
187187
!/docs/jira-import-fidelity-gap-analysis.md
188+
!/docs/item-put-change-feed-audit-2026-08-25.md
188189
!/docs/WI-531-public-forms-backend-api-audit.md
189190
!/docs/WI-523-public-forms-completion.md
190191
!/docs/xray-jira-test-case-import.md

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ It ships as a single Go binary with the Svelte frontend built in.
3838

3939
## Get started
4040

41-
[Download the latest release](https://windshift.sh/download), then follow the [quick start guide](https://windshift.sh/docs/01-getting-started/02-quick-start). Windshift is designed to run comfortably on anything from a Raspberry Pi to a dedicated server.
41+
[Download the latest release](https://windshift.sh/download), then follow the [quick start guide](https://windshift.sh/self-hosting/01-getting-started/02-quick-start). Windshift is designed to run comfortably on anything from a Raspberry Pi to a dedicated server.
4242

4343
Want to build from source? See [BUILD.md](BUILD.md). For local development and contribution guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md).
4444

api/openapi.json

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,32 @@
10081008
},
10091009
"type": "object"
10101010
},
1011+
"internal_restapi_v1_handlers.batchItemLinksResponse": {
1012+
"properties": {
1013+
"has_more_links": {
1014+
"type": "boolean"
1015+
},
1016+
"incoming": {
1017+
"items": {
1018+
"$ref": "#/components/schemas/models.ItemLink"
1019+
},
1020+
"type": "array"
1021+
},
1022+
"item_id": {
1023+
"type": "integer"
1024+
},
1025+
"next_after_link_id": {
1026+
"type": "integer"
1027+
},
1028+
"outgoing": {
1029+
"items": {
1030+
"$ref": "#/components/schemas/models.ItemLink"
1031+
},
1032+
"type": "array"
1033+
}
1034+
},
1035+
"type": "object"
1036+
},
10111037
"internal_restapi_v1_handlers.capabilityDTO": {
10121038
"properties": {
10131039
"capability_type": {
@@ -13074,6 +13100,153 @@
1307413100
]
1307513101
}
1307613102
},
13103+
"/links/batch": {
13104+
"get": {
13105+
"description": "Selects up to 100 visible anchor items by CQL or explicit ids and returns at most 50 direct item-to-item links per anchor. The operation is one hop only. Supply exactly one of `ql` or `ids`. When an item has more links, request that single item with `ids` and `after_id` to continue.",
13106+
"parameters": [
13107+
{
13108+
"description": "CQL selecting anchor items",
13109+
"in": "query",
13110+
"name": "ql",
13111+
"schema": {
13112+
"type": "string"
13113+
}
13114+
},
13115+
{
13116+
"description": "Comma-separated anchor item ids (max 100)",
13117+
"in": "query",
13118+
"name": "ids",
13119+
"schema": {
13120+
"type": "string"
13121+
}
13122+
},
13123+
{
13124+
"description": "CQL item page (1-based)",
13125+
"in": "query",
13126+
"name": "page",
13127+
"schema": {
13128+
"type": "integer"
13129+
}
13130+
},
13131+
{
13132+
"description": "CQL items per page (max 100)",
13133+
"in": "query",
13134+
"name": "limit",
13135+
"schema": {
13136+
"type": "integer"
13137+
}
13138+
},
13139+
{
13140+
"description": "CQL item sort field",
13141+
"in": "query",
13142+
"name": "sort",
13143+
"schema": {
13144+
"type": "string"
13145+
}
13146+
},
13147+
{
13148+
"description": "CQL item sort order: asc or desc",
13149+
"in": "query",
13150+
"name": "order",
13151+
"schema": {
13152+
"type": "string"
13153+
}
13154+
},
13155+
{
13156+
"description": "Exclusive link cursor; valid only with one explicit item id",
13157+
"in": "query",
13158+
"name": "after_id",
13159+
"schema": {
13160+
"type": "integer"
13161+
}
13162+
},
13163+
{
13164+
"description": "Include links managed by custom fields",
13165+
"in": "query",
13166+
"name": "include_custom_fields",
13167+
"schema": {
13168+
"type": "boolean"
13169+
}
13170+
}
13171+
],
13172+
"responses": {
13173+
"200": {
13174+
"content": {
13175+
"application/json": {
13176+
"schema": {
13177+
"allOf": [
13178+
{
13179+
"$ref": "#/components/schemas/internal_restapi_v1_handlers.PaginatedResponse"
13180+
},
13181+
{
13182+
"properties": {
13183+
"data": {
13184+
"items": {
13185+
"$ref": "#/components/schemas/internal_restapi_v1_handlers.batchItemLinksResponse"
13186+
},
13187+
"type": "array"
13188+
}
13189+
},
13190+
"type": "object"
13191+
}
13192+
]
13193+
}
13194+
}
13195+
},
13196+
"description": "OK"
13197+
},
13198+
"400": {
13199+
"content": {
13200+
"application/json": {
13201+
"schema": {
13202+
"$ref": "#/components/schemas/internal_restapi_v1_handlers.ErrorResponse"
13203+
}
13204+
}
13205+
},
13206+
"description": "Bad Request"
13207+
},
13208+
"401": {
13209+
"content": {
13210+
"application/json": {
13211+
"schema": {
13212+
"$ref": "#/components/schemas/internal_restapi_v1_handlers.ErrorResponse"
13213+
}
13214+
}
13215+
},
13216+
"description": "Unauthorized"
13217+
},
13218+
"403": {
13219+
"content": {
13220+
"application/json": {
13221+
"schema": {
13222+
"$ref": "#/components/schemas/internal_restapi_v1_handlers.ErrorResponse"
13223+
}
13224+
}
13225+
},
13226+
"description": "Forbidden"
13227+
},
13228+
"500": {
13229+
"content": {
13230+
"application/json": {
13231+
"schema": {
13232+
"$ref": "#/components/schemas/internal_restapi_v1_handlers.ErrorResponse"
13233+
}
13234+
}
13235+
},
13236+
"description": "Internal Server Error"
13237+
}
13238+
},
13239+
"security": [
13240+
{
13241+
"BearerAuth": []
13242+
}
13243+
],
13244+
"summary": "List direct links for a batch of items",
13245+
"tags": [
13246+
"links"
13247+
]
13248+
}
13249+
},
1307713250
"/links/{id}": {
1307813251
"delete": {
1307913252
"parameters": [

api/openapi.yaml

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,23 @@ components:
664664
name:
665665
type: string
666666
type: object
667+
internal_restapi_v1_handlers.batchItemLinksResponse:
668+
properties:
669+
has_more_links:
670+
type: boolean
671+
incoming:
672+
items:
673+
$ref: '#/components/schemas/models.ItemLink'
674+
type: array
675+
item_id:
676+
type: integer
677+
next_after_link_id:
678+
type: integer
679+
outgoing:
680+
items:
681+
$ref: '#/components/schemas/models.ItemLink'
682+
type: array
683+
type: object
667684
internal_restapi_v1_handlers.capabilityDTO:
668685
properties:
669686
capability_type:
@@ -8422,6 +8439,96 @@ paths:
84228439
summary: Create a cross-entity link
84238440
tags:
84248441
- links
8442+
/links/batch:
8443+
get:
8444+
description: Selects up to 100 visible anchor items by CQL or explicit ids and
8445+
returns at most 50 direct item-to-item links per anchor. The operation is
8446+
one hop only. Supply exactly one of `ql` or `ids`. When an item has more links,
8447+
request that single item with `ids` and `after_id` to continue.
8448+
parameters:
8449+
- description: CQL selecting anchor items
8450+
in: query
8451+
name: ql
8452+
schema:
8453+
type: string
8454+
- description: Comma-separated anchor item ids (max 100)
8455+
in: query
8456+
name: ids
8457+
schema:
8458+
type: string
8459+
- description: CQL item page (1-based)
8460+
in: query
8461+
name: page
8462+
schema:
8463+
type: integer
8464+
- description: CQL items per page (max 100)
8465+
in: query
8466+
name: limit
8467+
schema:
8468+
type: integer
8469+
- description: CQL item sort field
8470+
in: query
8471+
name: sort
8472+
schema:
8473+
type: string
8474+
- description: 'CQL item sort order: asc or desc'
8475+
in: query
8476+
name: order
8477+
schema:
8478+
type: string
8479+
- description: Exclusive link cursor; valid only with one explicit item id
8480+
in: query
8481+
name: after_id
8482+
schema:
8483+
type: integer
8484+
- description: Include links managed by custom fields
8485+
in: query
8486+
name: include_custom_fields
8487+
schema:
8488+
type: boolean
8489+
responses:
8490+
"200":
8491+
content:
8492+
application/json:
8493+
schema:
8494+
allOf:
8495+
- $ref: '#/components/schemas/internal_restapi_v1_handlers.PaginatedResponse'
8496+
- properties:
8497+
data:
8498+
items:
8499+
$ref: '#/components/schemas/internal_restapi_v1_handlers.batchItemLinksResponse'
8500+
type: array
8501+
type: object
8502+
description: OK
8503+
"400":
8504+
content:
8505+
application/json:
8506+
schema:
8507+
$ref: '#/components/schemas/internal_restapi_v1_handlers.ErrorResponse'
8508+
description: Bad Request
8509+
"401":
8510+
content:
8511+
application/json:
8512+
schema:
8513+
$ref: '#/components/schemas/internal_restapi_v1_handlers.ErrorResponse'
8514+
description: Unauthorized
8515+
"403":
8516+
content:
8517+
application/json:
8518+
schema:
8519+
$ref: '#/components/schemas/internal_restapi_v1_handlers.ErrorResponse'
8520+
description: Forbidden
8521+
"500":
8522+
content:
8523+
application/json:
8524+
schema:
8525+
$ref: '#/components/schemas/internal_restapi_v1_handlers.ErrorResponse'
8526+
description: Internal Server Error
8527+
security:
8528+
- BearerAuth: []
8529+
summary: List direct links for a batch of items
8530+
tags:
8531+
- links
84258532
/links/{id}:
84268533
delete:
84278534
parameters:
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Item PUT and change-feed audit
2+
3+
Date: 2026-08-25
4+
5+
## Scope
6+
7+
Reviewed cookie API and bearer v1 `PUT` routes whose primary resource is a work
8+
item or an item-owned subresource. Also reviewed label assignment and generic
9+
link create/delete paths because GitHub issue #219 names them explicitly.
10+
11+
The relevant contracts are:
12+
13+
- `items.updated_at` changes when the serialized shared item payload changes.
14+
- `items.last_active_at` changes for card activity, but not for manual ordering
15+
or catalog-wide metadata maintenance.
16+
- Every committed item-row update emits an `item_change_log` row through the
17+
SQLite/PostgreSQL schema triggers used by `/items/changes`.
18+
19+
## Results
20+
21+
| Operation | Shared item payload | `updated_at` | `last_active_at` | Change feed | Result |
22+
| --- | --- | --- | --- | --- | --- |
23+
| `PUT /items/{id}` | Changes | Yes | Yes | Yes | Existing shared update service is correct. |
24+
| `PUT /items/{id}/labels` | Changes | Yes | Yes | Yes | Fixed in the shared label repository; cookie, v1, CLI, and AI paths inherit it. |
25+
| `POST/DELETE /items/{id}/labels...` | Changes | Yes | Yes | Yes | Fixed with the same transactional assignment contract. A missing-label removal remains a no-op. |
26+
| `PUT /labels/{id}` | Changes labels nested in assigned items | Yes | No | Yes | Fixed by invalidating assigned items in the label transaction without bubbling every card. |
27+
| `DELETE /labels/{id}` | Removes labels nested in assigned items | Yes | No | Yes | Fixed before the assignment cascade in the same transaction. |
28+
| `POST/DELETE /links...` | Changes item detail links | Yes, for every item endpoint | Yes, for every item endpoint | Yes | Fixed transactionally. Single-value replacement also invalidates the removed target. |
29+
| `PUT /items/{id}/frac-index` | Changes manual order only | No | No | Yes | Intentional. The item-row trigger propagates ordering while timestamps stay stable. |
30+
| `PUT /comments/{id}` | No; comment is fetched separately | No | Yes | Yes | Intentional. Comment activity touches the item and has its own resource timestamp. |
31+
| `PUT /items/{id}/recurrence` | No; recurrence is fetched separately | No | No | No | No parent invalidation required by the item payload contract. |
32+
| `PUT /diagrams/{id}` | No; diagrams are fetched separately | No | No | No | No parent invalidation required; diagram history and timestamps are recorded on the diagram resource. |
33+
| `PUT /items/{id}/personal-labels` | Viewer-specific data | No | No | No | Intentional. Per-user metadata must not mutate shared item activity for every viewer. |
34+
35+
## Transaction boundaries
36+
37+
Label assignments now update the junction table and item timestamps in one
38+
transaction. Generic link creation, deletion, and single-value replacement do
39+
the same for every affected item endpoint. A timestamp/change-feed failure
40+
therefore rolls back the relationship mutation instead of returning a partial
41+
success.
42+
43+
Jira import continues to attach labels without touching item activity so the
44+
imported source timestamps remain intact. GitHub issue sync already replaces
45+
labels inside the transaction that updates the item itself.

0 commit comments

Comments
 (0)