Skip to content

Commit db76fe6

Browse files
committed
Merge latest main into item deletion feedback fix
2 parents f88c730 + 8608da0 commit db76fe6

102 files changed

Lines changed: 2416 additions & 1216 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/go.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ jobs:
9191
go-version-file: go.mod
9292
cache: true
9393

94+
- name: Set up Node.js
95+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
96+
with:
97+
node-version-file: '.nvmrc'
98+
9499
- name: Verify handler annotations parse and produce valid OpenAPI 3.0
95100
run: make openapi-check
96101
# Fails when swag can't parse an annotation, or when the resulting

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ openapi-v2-check:
172172
@go run ./scripts/openapi-v2-generate -spec api/openapi-v2.json -check
173173
@go run ./scripts/openapi-v2-check -spec api/openapi-v2.json
174174
@$(MAKE) --no-print-directory openapi-v2-client-smoke
175+
@node scripts/check-frontend-v2-fields.mjs
175176

176177
openapi-v2-client-smoke:
177178
@tmpdir=$$(mktemp -d /tmp/windshift-v2-client.XXXXXX) && trap 'rm -rf "$$tmpdir"' EXIT && \

api/openapi-v2.json

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18165,6 +18165,10 @@
1816518165
"description": "The completed value.",
1816618166
"type": "integer"
1816718167
},
18168+
"completed_points": {
18169+
"description": "The completed points value.",
18170+
"type": "number"
18171+
},
1816818172
"date": {
1816918173
"description": "The date value.",
1817018174
"type": "string"
@@ -18173,16 +18177,27 @@
1817318177
"description": "The ideal value.",
1817418178
"type": "integer"
1817518179
},
18180+
"ideal_points": {
18181+
"description": "The ideal points value.",
18182+
"type": "number"
18183+
},
1817618184
"remaining": {
1817718185
"description": "The remaining value.",
1817818186
"type": "integer"
18187+
},
18188+
"remaining_points": {
18189+
"description": "The remaining points value.",
18190+
"type": "number"
1817918191
}
1818018192
},
1818118193
"required": [
1818218194
"completed",
18195+
"completed_points",
1818318196
"date",
1818418197
"ideal",
18185-
"remaining"
18198+
"ideal_points",
18199+
"remaining",
18200+
"remaining_points"
1818618201
],
1818718202
"type": "object"
1818818203
},
@@ -36776,6 +36791,17 @@
3677636791
"nullable": true,
3677736792
"type": "string"
3677836793
},
36794+
"estimate_minutes": {
36795+
"description": "Time estimate in minutes.",
36796+
"minimum": 0,
36797+
"nullable": true,
36798+
"type": "integer"
36799+
},
36800+
"inherit_project": {
36801+
"default": false,
36802+
"description": "Inherit time-project membership from the parent.",
36803+
"type": "boolean"
36804+
},
3677936805
"is_task": {
3678036806
"description": "Replacement personal-task flag. True requires the item to be in the caller's own personal workspace with status Open or Done; null is invalid.",
3678136807
"type": "boolean"
@@ -36820,6 +36846,13 @@
3682036846
"nullable": true,
3682136847
"type": "string"
3682236848
},
36849+
"story_points": {
36850+
"description": "Relative effort estimate.",
36851+
"format": "double",
36852+
"minimum": 0,
36853+
"nullable": true,
36854+
"type": "number"
36855+
},
3682336856
"title": {
3682436857
"description": "Replacement plain-text title.",
3682536858
"maxLength": 255,
@@ -39410,6 +39443,16 @@
3941039443
"nullable": true,
3941139444
"type": "string"
3941239445
},
39446+
"estimate_minutes": {
39447+
"description": "The estimate minutes value.",
39448+
"nullable": true,
39449+
"type": "integer"
39450+
},
39451+
"inherit_project": {
39452+
"description": "The inherit project value.",
39453+
"nullable": true,
39454+
"type": "boolean"
39455+
},
3941339456
"is_task": {
3941439457
"description": "Whether the item is a personal task. True is valid only in the authenticated caller's personal workspace and only with the Open or Done system status.",
3941539458
"nullable": true,
@@ -39449,6 +39492,11 @@
3944939492
"nullable": true,
3945039493
"type": "string"
3945139494
},
39495+
"story_points": {
39496+
"description": "The story points value.",
39497+
"nullable": true,
39498+
"type": "number"
39499+
},
3945239500
"title": {
3945339501
"description": "The title value.",
3945439502
"nullable": true,
@@ -63378,6 +63426,11 @@
6337863426
"description": "The parent id value.",
6337963427
"nullable": true,
6338063428
"type": "integer"
63429+
},
63430+
"sort_order": {
63431+
"description": "The sort order value.",
63432+
"nullable": true,
63433+
"type": "integer"
6338163434
}
6338263435
},
6338363436
"required": [
@@ -71668,6 +71721,17 @@
7166871721
"default": false,
7166971722
"type": "boolean"
7167071723
}
71724+
},
71725+
{
71726+
"description": "For completed items, include only those active within this many days. Incomplete items remain included.",
71727+
"in": "query",
71728+
"name": "completed_activity_days",
71729+
"required": false,
71730+
"schema": {
71731+
"maximum": 3650,
71732+
"minimum": 1,
71733+
"type": "integer"
71734+
}
7167171735
}
7167271736
],
7167371737
"responses": {
@@ -75475,6 +75539,15 @@
7547575539
"format": "date",
7547675540
"type": "string"
7547775541
}
75542+
},
75543+
{
75544+
"description": "IANA timezone that from/to are interpreted in; defaults to the caller's profile timezone.",
75545+
"in": "query",
75546+
"name": "timezone",
75547+
"required": false,
75548+
"schema": {
75549+
"type": "string"
75550+
}
7547875551
}
7547975552
],
7548075553
"responses": {
@@ -79679,6 +79752,15 @@
7967979752
"format": "date",
7968079753
"type": "string"
7968179754
}
79755+
},
79756+
{
79757+
"description": "IANA timezone that from/to are interpreted in; defaults to the caller's profile timezone.",
79758+
"in": "query",
79759+
"name": "timezone",
79760+
"required": false,
79761+
"schema": {
79762+
"type": "string"
79763+
}
7968279764
}
7968379765
],
7968479766
"responses": {
@@ -79933,6 +80015,15 @@
7993380015
"format": "date",
7993480016
"type": "string"
7993580017
}
80018+
},
80019+
{
80020+
"description": "IANA timezone that from/to are interpreted in; defaults to the caller's profile timezone.",
80021+
"in": "query",
80022+
"name": "timezone",
80023+
"required": false,
80024+
"schema": {
80025+
"type": "string"
80026+
}
7993680027
}
7993780028
],
7993880029
"responses": {

docs/collection-query-contract.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Collection query and pagination contract
2+
3+
A saved collection is a query over the caller's accessible workspaces. An empty
4+
or whitespace-only query adds no restriction. A collection's workspace location
5+
does not replace its query scope; explicit workspace restrictions belong in CQL.
6+
Completion filters and other sub-filters narrow that same scope before counting
7+
and pagination. Removing a sub-filter must not turn an unrestricted collection
8+
into an empty result.
9+
10+
Item lists and backlog resolve saved queries through `resolveItemListQLContext`.
11+
Delta membership checks use the item list service. Board metadata projects the
12+
matching workspace IDs through the same CQL evaluator and permission scope,
13+
including when the query is empty.
14+
15+
The v2 response contract uses `page`, `page_size`, `total_items`, and `total_pages`.
16+
`collectionService.js` adapts `page_size` to the collection store's existing
17+
`limit` option once, for both items and backlog. Continuations must use that
18+
effective server size, especially when the server caps a requested size.
19+
Headers, pagination controls, and remaining counts all use `total_items`.
20+
The collection query editor opts into empty searches and consumes canonical v2
21+
pagination directly. The general search page still waits for a query.
22+
23+
Regression coverage lives in the sibling `core-tests` repository:
24+
25+
- `tests/collection_empty_query_test.go`: empty and whitespace queries,
26+
completion sub-filters, page contents and totals, board metadata, and
27+
inaccessible workspace exclusion.
28+
- `frontend/src/lib/features/collections/collectionService.test.js`: the v2
29+
pagination response adapter for item and backlog continuation.
30+
- `e2e/tests/collection-empty-query.spec.ts`: navigation beyond fifty rows,
31+
both list and query-editor pagination, toggling completion visibility from
32+
page two, and preference persistence.
33+
- `frontend/src/lib/stores/searchStore.test.js`: unrestricted empty collection
34+
searches and the general search page's initial idle state.
35+
36+
## Validation commands
37+
38+
Run from `core`:
39+
40+
```sh
41+
../core-tests/overlay.sh . -- -tags=test -count=1 -run 'TestEmptyCollection|TestCollectionMetadata|TestItemsBatch' ./tests
42+
TEST_DB_TYPE=postgres TEST_POSTGRES_DSN='postgresql://localhost:5432/postgres?sslmode=disable' ../core-tests/overlay.sh . -- -tags=test -count=1 -run 'TestEmptyCollection|TestCollectionMetadata|TestItemsBatch' ./tests
43+
../core-tests/overlay.sh . -- -tags=test -count=1 -run 'TestItemCRUDService|TestCollection.*Board|TestBoardConfiguration' ./internal/services
44+
../core-tests/run-overlay-script.sh . scripts/run-frontend-tests.sh src/lib/features/collections src/lib/stores/collectionContext.test.js src/lib/stores/collectionCompletion.test.js src/lib/stores/searchStore.test.js
45+
E2E_KEEP_ARTIFACTS=1 ../core-tests/run-e2e.sh tests/collection-empty-query.spec.ts tests/collection-completed-visibility.spec.ts tests/collection-search-columns.spec.ts --retries=0
46+
./scripts/run-golangci-lint.sh run --timeout=5m
47+
```
48+
49+
Go files are formatted with `gofmt`. Changed JavaScript and TypeScript files are
50+
checked with Biome using `frontend/biome.json`; Svelte files are excluded.
51+
Full repository suites are outside this focused validation.

docs/v2-bughunt-validation.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# V2 transition bug fixes
2+
3+
The fixes cover batch-link endpoint permissions, asset picker query translation,
4+
workspace planning records in unscoped frontend lists, empty batch-link query
5+
pages, and merge-patch validation in the frontend contract guard.
6+
7+
Regression tests live in `../core-tests`. Before the production fixes, focused
8+
tests reproduced the link disclosure, empty-query error, missing asset filter,
9+
missing workspace planning records, and undetected PATCH field. The guard test
10+
also exposed leading whitespace causing the first payload field to be skipped.
11+
12+
## Passing validation
13+
14+
Commands run from `core`, unless otherwise noted:
15+
16+
```sh
17+
../core-tests/overlay.sh . -- -tags=test -count=1 ./internal/restapi/v2/... ./internal/services/...
18+
../core-tests/overlay.sh . -- -tags=test -count=1 -run 'TestBatchLinks|TestListOneHopItemLinksPage' ./internal/services/...
19+
env TEST_DB_TYPE=postgres TEST_POSTGRES_DSN='host=/tmp user=stefanernst dbname=postgres sslmode=disable' ../core-tests/overlay.sh . -- -tags=test -count=1 -run 'TestBatchLinks' ./internal/services/...
20+
../core-tests/overlay.sh . -- -tags=test -count=1 -run 'TestV2BatchLinks|TestV2LinksBatchIncludesTestCaseLinks' ./tests/...
21+
../core-tests/run-overlay-script.sh . scripts/run-frontend-tests.sh src/lib/api
22+
bun run scripts/check-frontend-v2-fields.mjs
23+
./scripts/run-golangci-lint.sh run ./internal/services/...
24+
git diff --check
25+
```
26+
27+
The frontend API suite passed 381 tests. The field guard checked 31 literals.
28+
HTTP tests verify the hidden-link response on both session and bearer mounts,
29+
the successful visible-link path, and the exact empty-query page envelope.
30+
The PostgreSQL regressions cover non-item visibility and filtered pagination;
31+
the broader Go suites ran on SQLite.
32+
33+
From `core/frontend`:
34+
35+
```sh
36+
bun --bun node_modules/.bin/biome check --write ../scripts/check-frontend-v2-fields.mjs ../../core-tests/frontend/src/lib/api/v2Bughunt.test.js ../../core-tests/frontend/src/lib/api/v2FieldGuard.test.js src/lib/api/milestones.js src/lib/api/assets.js
37+
```
38+
39+
Go changes and new Go tests were formatted with `gofmt -w`. Go lint and Biome
40+
completed without findings. Sandbox failures accessing the Go and npm caches
41+
were rerun outside the sandbox. PostgreSQL validation caught and resolved a
42+
CASE-expression parameter type issue before passing.
43+
44+
Browser tests, the full frontend suite, the full HTTP suite, and a full
45+
PostgreSQL suite were not run. Unscoped planning lists now enumerate accessible
46+
workspaces, so they make additional requests proportional to workspace count.

docs/worklog-time-model.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Worklog time model (0.8.8)
2+
3+
Fixes WI-1279, WI-1280, WI-1281, WI-1282 (report: GitHub issue 247).
4+
5+
## Agreed model
6+
7+
A worklog entry is constructed from a selected civil date, optional clock
8+
times, and an explicit entry timezone. The three fields together resolve to
9+
complete UTC start/end timestamps, which are the single source of truth for
10+
display, filtering, and reporting.
11+
12+
- Explicit `start_time`/`end_time` clocks take precedence over a supplied
13+
`duration`; when both are sent the duration must match or the request is
14+
rejected with 400 (`duration does not match start_time and end_time`).
15+
- `start_time` plus `duration` computes the end clock on the same civil date.
16+
- An end clock at or before the start clock resolves on the following day, so
17+
overnight work (23:00-01:00) is one continuous interval. Equal clocks span
18+
the full 24h daily cap.
19+
- Duration-only input keeps the established convention: the interval starts
20+
at local midnight on the selected date in the entry timezone.
21+
- Clocks are exact wall-clock values: DST gaps and folds are rejected with an
22+
explicit error, and the offset of each actual date applies (an overnight
23+
entry across a DST boundary stores the true elapsed time).
24+
- Moving the selected date on an edit reconstructs both timestamps from the
25+
new day, preserving the clock times and the day difference.
26+
- A PATCH that touches no time field preserves the stored interval exactly.
27+
The frontend form sends time fields only when the user changed one, and
28+
always sends its explicit `timezone` when it does.
29+
30+
## Entry timezone resolution
31+
32+
`input.timezone` wins, then the acting user's profile timezone, then the
33+
stored profile default (UTC). The frontend populates and interprets the form
34+
controls in the profile timezone, so browser/profile differences cannot
35+
silently reinterpret saved work.
36+
37+
## Reporting semantics (WI-1281)
38+
39+
Date labels, date-range inclusion, and daily totals derive from the stored
40+
timestamps in an explicitly selected reporting timezone:
41+
42+
- `GET /time/worklogs`, `GET /time/projects/{id}/worklogs`, and v1
43+
`GET /rest/api/v1/time/worklogs` accept a `timezone` query parameter for
44+
their civil date filters; it defaults to the caller's profile timezone.
45+
- Filters are interval overlaps on the timestamps (`start_time < rangeEnd`
46+
and `end_time > rangeStart`), so an entry crossing local midnight counts
47+
toward both days. An interval ending exactly at midnight contributes
48+
nothing to the following day.
49+
- List ordering is `start_time DESC`.
50+
- Desktop views (TimeEntry, Timesheet, TimeReports), exports (CSV/PDF), the
51+
mobile timer list, and item worklog tabs label and group by the civil date
52+
of the timestamps in the reporting timezone, splitting minutes at local
53+
midnight (`frontend/src/lib/utils/worklogTimezone.js`).
54+
- The daily briefing query uses the same overlap semantics against the
55+
user-timezone day window.
56+
57+
## Stored `date` column inventory (kept, not dropped)
58+
59+
`time_worklogs.date` still stores the UTC-midnight key of the entry date in
60+
the entry timezone, and writers still populate it (worklog create/update,
61+
timer stop, AI `log_time`). Remaining readers after 0.8.8:
62+
63+
- API responses (`date` field on v1 and v2 DTOs) for external consumers.
64+
- No internal grouping, filtering, or ordering reads the column anymore; the
65+
daily briefing and every report derive from the timestamps.
66+
67+
Dropping the column requires a migration plus an audit of external API
68+
consumers of the `date` field; historical rows keep valid timestamps, so no
69+
backfill is needed before that removal.

frontend/src/App.svelte

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -506,10 +506,6 @@
506506
background-color: var(--ds-background-neutral-hovered);
507507
}
508508
509-
:global(.themed-nav .nav-button.nav-button-emphasized) {
510-
background-color: color-mix(in srgb, var(--ds-interactive) 8%, transparent);
511-
}
512-
513509
/* Exception: Primary buttons should keep their original colors and hover behavior */
514510
:global(.themed-nav .bg-primary) {
515511
color: var(--ds-text-inverse) !important;

frontend/src/lib/api/actions.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fetchV2Data } from './core.js';
1+
import { fetchAllV2Pages, fetchV2Data } from './core.js';
22

33
function actionMutationData(data) {
44
const {
@@ -56,7 +56,7 @@ export const actions = {
5656
body: JSON.stringify({ item_id: itemId }),
5757
}),
5858
getLogs: (workspaceId, actionId) =>
59-
fetchV2Data(`/workspaces/${workspaceId}/actions/${actionId}/logs`),
59+
fetchAllV2Pages(`/workspaces/${workspaceId}/actions/${actionId}/logs`),
6060
};
6161

6262
// Action templates: read-only registry shipped with the binary, plus

0 commit comments

Comments
 (0)