labtasker-webui 0.1.1__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/.agents/sops/qa.md +15 -1
  2. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/AGENTS.md +1 -1
  3. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/PKG-INFO +12 -2
  4. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/README.md +10 -1
  5. labtasker_webui-0.2.1/assets/screenshot.png +0 -0
  6. labtasker_webui-0.2.1/docs/reference/specification.md +252 -0
  7. labtasker_webui-0.2.1/docs/releases/v0.2.0.md +14 -0
  8. labtasker_webui-0.2.1/docs/releases/v0.2.1.md +6 -0
  9. labtasker_webui-0.2.1/frontend/dist/assets/index-8PV1Mfzp.css +1 -0
  10. labtasker_webui-0.2.1/frontend/dist/assets/index-D0KWa0Gc.js +56 -0
  11. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/frontend/dist/index.html +3 -3
  12. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/frontend/package-lock.json +2 -2
  13. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/frontend/package.json +1 -1
  14. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/pyproject.toml +3 -2
  15. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/__init__.py +1 -1
  16. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/app.py +119 -3
  17. labtasker_webui-0.2.1/src/labtasker_webui/compatibility.py +27 -0
  18. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/profile.py +1 -0
  19. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/schemas.py +28 -0
  20. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/upstream.py +3 -0
  21. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/tests/fixture_server.py +38 -3
  22. labtasker_webui-0.2.1/tests/test_compatibility.py +75 -0
  23. labtasker_webui-0.2.1/tests/test_name_fuzzy.py +39 -0
  24. labtasker_webui-0.2.1/tests/test_observations.py +54 -0
  25. labtasker_webui-0.2.1/tests/test_upstream_source.py +100 -0
  26. labtasker_webui-0.1.1/assets/screenshot.png +0 -0
  27. labtasker_webui-0.1.1/docs/reference/specification.md +0 -77
  28. labtasker_webui-0.1.1/frontend/dist/assets/index-CscGvBx9.css +0 -1
  29. labtasker_webui-0.1.1/frontend/dist/assets/index-D2PfKj6B.js +0 -54
  30. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/.agents/skills/release/SKILL.md +0 -0
  31. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/.agents/skills/ui-change/SKILL.md +0 -0
  32. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/.agents/sops/development.md +0 -0
  33. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/.agents/sops/releasing.md +0 -0
  34. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/.gitignore +0 -0
  35. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/CONTRIBUTING.md +0 -0
  36. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/LICENSE +0 -0
  37. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/frontend/dist/labtasker-icon.png +0 -0
  38. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/scripts/check_package.py +0 -0
  39. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/scripts/check_versions.py +0 -0
  40. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/__main__.py +0 -0
  41. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/cli.py +0 -0
  42. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/config.py +0 -0
  43. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/local.py +0 -0
  44. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/operations.py +0 -0
  45. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/py.typed +0 -0
  46. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/security.py +0 -0
  47. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/src/labtasker_webui/sessions.py +0 -0
  48. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/tests/packaging_smoke.py +0 -0
  49. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/tests/test_app.py +0 -0
  50. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/tests/test_config.py +0 -0
  51. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/tests/test_custom_columns.py +0 -0
  52. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/tests/test_local.py +0 -0
  53. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/tests/test_operations.py +0 -0
  54. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/tests/test_profile.py +0 -0
  55. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/tests/test_real_server.py +0 -0
  56. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/tests/test_release_versions.py +0 -0
  57. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/tests/test_sessions.py +0 -0
  58. {labtasker_webui-0.1.1 → labtasker_webui-0.2.1}/tests/test_upstream.py +0 -0
@@ -37,6 +37,20 @@ LABTASKER_REAL_SERVER_URL=http://127.0.0.1:8000 uv run pytest -m integration tes
37
37
  Set `LABTASKER_REAL_SERVER_TOKEN` through an existing secure mechanism if needed; never
38
38
  paste it into committed scripts or captured shell output.
39
39
 
40
+ To check a sibling Labtasker source checkout, run the isolated BFF contract test:
41
+
42
+ ```sh
43
+ uv run --with ../labtasker/packages/labtasker-server pytest -q -s -m integration tests/test_upstream_source.py
44
+ uv run --with-editable ../labtasker/packages/labtasker-server --with-editable ../labtasker/packages/labtasker-client pytest -q -s -m integration tests/test_upstream_source.py
45
+ ```
46
+
47
+ These commands use temporary dependency overlays without changing the lockfile. The
48
+ first checks the locked Client against the source Server; the second uses both source
49
+ packages in editable mode. The test creates only a temporary database and synthetic Tasks/Workers,
50
+ using in-process ASGI transport without connecting to a running instance. It covers
51
+ pagination, filters, counts, observations, error forwarding and Task actions. The
52
+ normal suite excludes it; without an installed Server it skips.
53
+
40
54
  ## Interaction review matrix
41
55
 
42
56
  | Area | Cases to verify |
@@ -44,7 +58,7 @@ paste it into committed scripts or captured shell output.
44
58
  | Layout | 1440×900 desktop, 900×600 short window, 390×700 narrow window; no document-level horizontal overflow |
45
59
  | Popups | Every edge; resize while open; many options; long names; internal scrolling; Escape, outside click and Tab |
46
60
  | Keyboard | Arrow keys/Home/End follow visible active option; Enter applies; IME Enter does not submit prematurely |
47
- | Filters | Dropdowns/presets immediate; text blur/Enter; clear; invalid input and correction; counts match applied selectors |
61
+ | Filters | Dropdowns/presets immediate; text blur/Enter; clear; invalid input and correction; status cards respect applied name/expression but ignore the Status selector; list totals include Status |
48
62
  | Navigation | Queue → filter → drawer → back/forward; URL and visible state agree |
49
63
  | Selection | Empty/partial/all checkbox; sorting retains selection; filtering clears selection; append keeps existing selection |
50
64
  | Columns | Independent resize; double-click fit; drag order; missing JSON paths blank; path casing preserved; reload restoration |
@@ -40,7 +40,7 @@
40
40
  - Preserve breadcrumbs and the compact, information-dense layout. Do not reintroduce a duplicate queue heading or large decorative spacing.
41
41
  - Queue layout and named views are scoped by connection identity **and** queue. Never reintroduce a global columns fallback.
42
42
  - Dropdowns and presets apply immediately. Text filters apply on Enter or leaving the input group; Apply is a fallback. Respect IME composition.
43
- - Counts describe the same applied selectors as the list. Sorting preserves selection; changing the filter range clears it.
43
+ - Status card counts use applied Task name and filter expressions, ignoring the Status selector; list totals also include the selected Status. Sorting preserves selection; changing the filter range clears it.
44
44
  - Browser history restores queue, applied filters and drawer consistently. Partial selection uses the native checkbox `indeterminate` property.
45
45
  - Resizing changes only the chosen column. Double-click fits content. Reordering preserves paths and uses a drop line, not a shaded target row.
46
46
  - Every popup must fit the viewport, support scrolling and Escape/outside dismissal, and remain usable with keyboard navigation. Check Safari/WebKit focus behavior, not only Chromium.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labtasker-webui
3
- Version: 0.1.1
3
+ Version: 0.2.1
4
4
  Summary: A focused web interface for Labtasker v2
5
5
  Project-URL: Homepage, https://github.com/luocfprime/labtasker-webui
6
6
  Project-URL: Repository, https://github.com/luocfprime/labtasker-webui
@@ -11,6 +11,7 @@ Requires-Python: >=3.11
11
11
  Requires-Dist: fastapi<1,>=0.115
12
12
  Requires-Dist: httpx[socks]<1,>=0.27
13
13
  Requires-Dist: labtasker-client<3,>=2.1
14
+ Requires-Dist: packaging<27,>=24
14
15
  Requires-Dist: uvicorn<1,>=0.34
15
16
  Provides-Extra: local
16
17
  Requires-Dist: labtasker-client<3,>=2.1; extra == 'local'
@@ -28,7 +29,11 @@ workspace, with reusable views for different experiments.
28
29
  The key features are:
29
30
 
30
31
  - **Queue progress at a glance:** Track pending, running, succeeded, failed, and
31
- cancelled Tasks. Status counts follow the same filters as the Task list.
32
+ cancelled Tasks. Status counts follow the applied Task name and advanced filter, across all statuses; the Status selector narrows only the Task list and its total.
33
+ - **Routes and Workers:** Collapse route navigation when you need more table space.
34
+ Inspect Idle/Busy Worker counts and last-seen reports; include inactive routes when
35
+ reviewing historical work. These features require Server 2.2 or later; Worker
36
+ reports require Workers that support observations.
32
37
  - **Flexible result exploration:** Filter Task fields and nested values, or add
33
38
  custom columns such as `args.test_num` and `result.succ_rate`. Resize, reorder,
34
39
  and double-click columns to fit their contents.
@@ -68,6 +73,11 @@ Choose a connection type on the connection page:
68
73
  `.` refers to the WebUI process's working directory. Local attachment uses the
69
74
  project's Unix socket and requires POSIX and a loopback bind.
70
75
 
76
+ If a business response reports a Server older than the installed Labtasker Client,
77
+ WebUI displays a dismissible upgrade warning. It adds no version-check request
78
+ and does not block browsing or actions. Servers that omit their version do not
79
+ trigger the warning. Reloading or reconnecting resets dismissal.
80
+
71
81
  Open a Queue, then filter its Tasks:
72
82
 
73
83
  ```python
@@ -10,7 +10,11 @@ workspace, with reusable views for different experiments.
10
10
  The key features are:
11
11
 
12
12
  - **Queue progress at a glance:** Track pending, running, succeeded, failed, and
13
- cancelled Tasks. Status counts follow the same filters as the Task list.
13
+ cancelled Tasks. Status counts follow the applied Task name and advanced filter, across all statuses; the Status selector narrows only the Task list and its total.
14
+ - **Routes and Workers:** Collapse route navigation when you need more table space.
15
+ Inspect Idle/Busy Worker counts and last-seen reports; include inactive routes when
16
+ reviewing historical work. These features require Server 2.2 or later; Worker
17
+ reports require Workers that support observations.
14
18
  - **Flexible result exploration:** Filter Task fields and nested values, or add
15
19
  custom columns such as `args.test_num` and `result.succ_rate`. Resize, reorder,
16
20
  and double-click columns to fit their contents.
@@ -50,6 +54,11 @@ Choose a connection type on the connection page:
50
54
  `.` refers to the WebUI process's working directory. Local attachment uses the
51
55
  project's Unix socket and requires POSIX and a loopback bind.
52
56
 
57
+ If a business response reports a Server older than the installed Labtasker Client,
58
+ WebUI displays a dismissible upgrade warning. It adds no version-check request
59
+ and does not block browsing or actions. Servers that omit their version do not
60
+ trigger the warning. Reloading or reconnecting resets dismissal.
61
+
53
62
  Open a Queue, then filter its Tasks:
54
63
 
55
64
  ```python
@@ -0,0 +1,252 @@
1
+ # WebUI specification
2
+
3
+ ```text
4
+ Browser (React, Query, Table)
5
+ -> same-origin /api/webui/* (FastAPI BFF)
6
+ -> HTTP Labtasker v2 Server, or existing project's Unix socket
7
+ ```
8
+
9
+ The BFF owns upstream credentials, session cookies, origin validation and destructive
10
+ operation snapshots. The UI observes Tasks and supports cancel, requeue and selected-ID
11
+ deletion; it does not submit/edit Tasks. Local attachment discovers and uses the existing
12
+ socket without managing the Labtasker process or opening its database.
13
+
14
+ ## Server version warnings
15
+
16
+ The BFF observes `Labtasker-Server-Version` on ordinary upstream `/api/`
17
+ responses, including errors and empty successes, for both HTTP and local
18
+ connections. Existing connection verification stays unchanged; version warnings
19
+ add no upstream requests. Health/schema responses are not version observations.
20
+
21
+ On a BFF response that observed an upstream business response, it sends
22
+ `Labtasker-Client-Version` (the installed `labtasker-client` version),
23
+ `Labtasker-Server-Version` (normalized PEP 440 version, or an empty value when
24
+ unknown), and `Labtasker-Server-Upgrade-Recommended` (`true` or `false`). Missing,
25
+ invalid, or longer-than-128-character upstream versions are unknown. Comparison
26
+ uses PEP 440 ordering, including patch and prerelease differences; the WebUI's
27
+ independent package version is not compared to the Server. Version observations
28
+ are request-local and must not leak across sessions or credentials. No tokens
29
+ are added to these headers; upstream payloads and errors remain unchanged.
30
+
31
+ The browser displays a compact, nonblocking, dismissible warning above the
32
+ workspace when the Server is older. It recommends upgrading to the Client version
33
+ or later without claiming that a particular operation is incompatible. Repeated
34
+ responses for a dismissed version pair do not redisplay it. A different older
35
+ version pair can warn again; an equal, newer, or unknown observation clears the
36
+ visible warning. Responses without an upstream observation leave it unchanged.
37
+ Observations and dismissal are memory-only and reset on reload/reconnection.
38
+ Neither credentials nor version observations are written to the profile.
39
+
40
+ ## State
41
+
42
+ | State | Owner and persistence |
43
+ | --- | --- |
44
+ | Connection URL/local project | Backend profile in WebUI working directory |
45
+ | Bearer token | Backend only; separate owner-only `webui-token` file |
46
+ | Queue, applied selectors, drawer Task ID | URL/history plus React state |
47
+ | Current working column layout | Connection + queue scope; browser and profile |
48
+ | Named data view | Explicit snapshot scoped to connection + queue |
49
+ | Task selection | Current workspace memory; exact Task IDs |
50
+ | Pending profile write | Browser recovery record until the backend acknowledges saving |
51
+ | Task data and counts | Query cache keyed by connection lifecycle, queue and applied selectors |
52
+
53
+ A profile contains a map of UI settings; it is not itself a named data view. Views capture
54
+ filters, sort order, visible/custom columns, order and widths. Layout edits do not mutate
55
+ a named view until Save. The backend UI-key allowlist must remain consistent with frontend
56
+ persistence keys; internal browser recovery keys must never enter profile API payloads.
57
+ Saved-view change detection compares setting values rather than object key order.
58
+ Older views without a route field are equivalent to an empty route selection, so
59
+ applying or resetting them does not falsely report unsaved changes.
60
+
61
+ Backend profile writes use atomic replacement and restrictive permissions. Failed writes
62
+ must not be reported as persisted by a subsequent API read. Browser pending writes are
63
+ retained across reload and merged before restoring older disk settings. Successful writes
64
+ acknowledge only values actually sent, preserving newer edits queued during the request.
65
+ Saves time out after 10 seconds and remain recoverable. Payloads above 60 KiB use normal
66
+ fetch instead of the browser-limited keepalive transport; interrupted writes stay in the
67
+ browser recovery record.
68
+
69
+ Successful login clears query caches and reloads connection status before rebuilding
70
+ the workspace. A new session must never expose the previous connection's cached Tasks,
71
+ including when reauthenticating with another Server or token.
72
+
73
+ ## Rendering and interaction
74
+
75
+ The table uses cursor loading plus row virtualization. Status card counts cover all Tasks
76
+ matching the applied Task name and advanced filter, ignoring the Status selector.
77
+ Explicit status conditions inside the advanced filter remain effective. Selecting a
78
+ Status changes the highlighted card, Task list and list total without narrowing the
79
+ other cards. List totals include all applied selectors, not just loaded rows.
80
+ Scrolling older rows pauses list polling to avoid reordering
81
+ what the user is reading. Refresh explicitly returns to the top and refreshes data/counts.
82
+ Task selection survives sorting, including browser Back/Forward through sort changes.
83
+ Applying or resetting a saved view also preserves selection when only sorting or
84
+ column layout changes.
85
+ Changing the filter range clears selection, including through browser history.
86
+ The selection column remains pinned while the Task table scrolls horizontally.
87
+ An explicit Task selection exposes Cancel, Requeue and Delete actions. Each action is
88
+ enabled only when every selected Task has a compatible status, using the same rules as
89
+ the Task drawer: Cancel accepts pending/running, Requeue accepts
90
+ pending/failed/cancelled, and Delete rejects running. Disabled actions explain the
91
+ required and incompatible statuses on hover or keyboard focus. Batch Cancel and Requeue
92
+ retain failed Task IDs for retry while clearing successful IDs from the selection.
93
+ Resizing uses per-column widths and a filler for remaining space. JSON paths are read-only
94
+ lookups; missing fields are blank.
95
+
96
+ The Task name input uses the upstream `name_fuzzy` selector: case-insensitive
97
+ Unicode subsequences, split on whitespace, with every word required and word
98
+ order unrestricted. Empty or whitespace-only searches add no restriction.
99
+ Punctuation is literal; no fzf extended operators or relevance sorting are added.
100
+ Text applies on Enter or blur, respecting IME composition. Exact lookup remains
101
+ available as `name == "..."` in Advanced filter. The URL and saved-view `name`
102
+ field store this input; previously saved name inputs now use fuzzy matching.
103
+
104
+ The BFF forwards `name_fuzzy` on list, count, and deletion-snapshot requests to
105
+ the Server. It does not filter loaded Tasks in the browser. Status cards ignore
106
+ the Status dropdown but retain name search and advanced filter; list totals use
107
+ all applied selectors. The existing selected-ID deletion UI is unchanged.
108
+ The snapshot endpoint requires at least one effective selector; whitespace-only
109
+ fuzzy input alone is insufficient. The upstream Server must support `name_fuzzy`;
110
+ there is no browser-only matching fallback.
111
+
112
+ Advanced filter help provides ten built-in examples covering routes, status, priority,
113
+ attempts, errors and custom fields. Route examples use exact-name array membership
114
+ (`"gpu-a100" in routes` and `"gpu-a100" not in routes`); combine checks with
115
+ `or` for any route or `and` for all. Selecting an example applies it immediately
116
+ alongside the status and Task name selectors.
117
+
118
+ View actions closes when keyboard focus leaves the view controls or moves to the
119
+ Data view picker, so its menu cannot remain open behind that picker. Shift+Tab from
120
+ the first menu item closes the menu and returns focus to the View actions trigger.
121
+ Shared popup positioning clamps to the viewport and chooses above/below placement.
122
+ Escape dismisses a selector from both its trigger and its optional action button,
123
+ returning focus to the trigger. Tab and Shift+Tab move between the trigger and its
124
+ action; Tab from the action leaves and dismisses the menu.
125
+ Selector letter navigation ignores Ctrl, Meta and Alt combinations so browser
126
+ shortcuts keep their normal behavior.
127
+ Keep focus inside menus during internal interactions; Safari can emit a blur with no
128
+ related target before a pointer click completes. Permanent request errors such as invalid
129
+ filters should appear immediately; only transient failures should retry.
130
+ Task action errors and pending indicators belong to the selected Task. Switching Tasks
131
+ resets those indicators without cancelling the previous action; its eventual response
132
+ invalidates the original Task cache, not the newly selected Task. Pending actions remain
133
+ tracked per Task across switching away and back, so returning cannot submit a duplicate.
134
+ Outside interactions dismiss the drawer by synchronously removing its Task from the
135
+ current history entry, allowing a clicked filter or navigation control to update history
136
+ without racing a Back operation. Focus stays with the outside control. The close button
137
+ and Escape retain normal drawer history navigation and restore the originating Task focus.
138
+
139
+ ## Build boundary
140
+
141
+ Vite produces `frontend/dist`. Hatch includes those assets in the wheel and sdist; the
142
+ installed wheel serves them without Node.js. A successful TypeScript build alone does not
143
+ prove packaging works. See [release verification](../../.agents/sops/releasing.md) for the separate artifact test.
144
+
145
+ ## Batch deletion recovery
146
+
147
+ Starting, retrying failed Tasks and stopping a batch each disable conflicting actions
148
+ while their request is pending. A failed progress read offers Retry status without
149
+ submitting another deletion, and allows returning to the list with an explicit notice
150
+ that the operation may still run. Stop failures remain visible. Closing the dialog
151
+ stops its polling; it does not cancel an operation already accepted by the backend.
152
+
153
+ The bounded operation store evicts completed records only. When all slots are active,
154
+ new operations receive a retryable capacity error; running operations remain queryable
155
+ and stoppable until completion.
156
+
157
+ ## Routes and Worker observations
158
+
159
+ Observation request failures appear as bottom-right notifications, outside the Routes
160
+ layout. Identical messages share one notification listing the affected observations;
161
+ Retry retries all affected active requests. Dismiss (or Escape while focused) keeps
162
+ that message quiet for the current Queue workspace, including refreshes and tab
163
+ switches. Successful requests clear their notification. Unavailable counts and Worker
164
+ observations remain marked in place after dismissal; Refresh remains available.
165
+ Corner notifications share a vertical stack with settings-save failures and transient
166
+ feedback, so recovery controls do not overlap. Notification actions leave the Task
167
+ drawer and its URL open. Modal dialogs remain above the notification stack.
168
+ Each new transient operation message receives a fresh five-second display duration;
169
+ an older timer cannot hide it early. Long column paths wrap within the viewport.
170
+ A notification stays visible while its Retry is pending. Pending retries belong to
171
+ active request identities: changing the Worker filter or tab hides inactive requests
172
+ and cannot disable a new filter's Retry, even when the error text matches. Escape
173
+ from a focused notification dismisses that notification without closing the Task
174
+ drawer; Escape from the drawer still closes the drawer. Worker append failures use
175
+ only the list's Retry loading control, which retries the failed cursor instead of
176
+ refreshing already-loaded pages.
177
+
178
+ Each Queue has Tasks and Workers tabs and a collapsible Routes sidebar. The sidebar
179
+ combines grouped pending/running Task routes with active Worker routes. Routes with
180
+ only terminal Tasks and no active Workers appear when Include inactive routes is on.
181
+ All group pages must load before absence is treated as zero. Unsupported or failed
182
+ observation requests remain explicit; they never imply that there are zero Workers.
183
+ A route is labeled Inactive only when both Task and Worker observations are available.
184
+ If Task observations fail, a known absence of Workers is labeled No active Workers
185
+ without inferring current Task demand from stale counts.
186
+ Existing Task browsing remains available on Servers without grouped counts.
187
+
188
+ Selecting a route intersects the Task filter with membership in `routes`, including
189
+ status counts, and filters Workers by `route`. Route and tab navigation use browser
190
+ history; named views include the selected route. Sidebar preferences belong to the
191
+ connection and Queue. Task summary controls use colored numbers followed by status
192
+ words on one line; they retain their status-filter toggle behavior.
193
+ Selecting the current tab or Worker status does not add a duplicate history entry
194
+ when the resulting URL is unchanged.
195
+
196
+ Worker state counts use grouped observations, independently of the paginated Worker
197
+ list. Observations describe reports, not Task ownership or execution guarantees.
198
+ Normal rows show last seen. Only after more than two 60-second reporting cycles
199
+ without a report does the row show delayed-update and expiry information. Expiry
200
+ uses the Server's `expires_at`; UI polling does not define the reporting interval.
201
+ Delayed-update and expiry messages wrap within their cell so narrow columns do not
202
+ hide the remaining time.
203
+ Failed list refreshes suppress expiry warnings and expose update/retry information.
204
+ Worker IDs, routes and times retain full-value hover tooltips; associated Tasks open
205
+ the existing detail drawer.
206
+ Switching between associated Task links keeps the drawer open. Explicitly closing it
207
+ restores focus to the most recently opened Worker Task link.
208
+
209
+ Change opens connection settings without discarding the current workspace. Back to
210
+ workspace restores it; a successful connection change clears the query cache.
211
+ Back to workspace is disabled while a connection change is pending. If the change
212
+ fails, returning to the previous workspace becomes available again.
213
+
214
+ The compact Task summary begins with All, the sum across the five mutually exclusive
215
+ statuses under the applied name, expression and route filters. Selecting All clears
216
+ only the Status selector. Task and Worker summary controls use a quiet rounded
217
+ selection fill. Worker Idle counts are green and Busy counts are blue. Worker status
218
+ selection uses the same styled, keyboard-accessible control as Task filters.
219
+
220
+ The workspace uses a white surface, medium-weight linked Task names, colored status
221
+ labels and row separators, without nested toolbar/table frames. Route collapse has
222
+ an explicit button and a 140 ms layout transition, disabled for reduced motion.
223
+ Route dots in the sidebar and Task/Worker tables describe current route observations:
224
+ blue for Busy Workers, green for Idle Workers, amber for pending/running Tasks without
225
+ active Workers, gray for inactive routes, and a hollow gray dot when counts are unknown.
226
+ Busy takes precedence over Idle when both are present. A route dot does not describe
227
+ the individual Task's status; each Task keeps its own separate status label.
228
+
229
+ The Routes divider supports pointer dragging and Left/Right keyboard adjustments.
230
+ Width is stored per connection and Queue, including across collapse and reload.
231
+ The sidebar is bounded to 160–480 px and at most 40% of the viewport. Dragging has
232
+ no layout animation; collapse keeps its short transition. On narrow screens the
233
+ Routes section stacks above content and its horizontal resize handle is hidden.
234
+
235
+ Task-name search has a 240 px preferred width and expands to its grid cell on narrow
236
+ screens. The Routes toggle stays beside the breadcrumb in both sidebar states. The sidebar
237
+ starts directly with All routes, aligned with the 36 px Task/Worker tab row; there
238
+ is no separate sidebar title or collapse button in the tab row. Selected status
239
+ summaries use a pale version of their status color with matching label text; All uses
240
+ neutral charcoal and a pale gray background. Typography uses 13 px controls, 12 px summary labels and 24 px count numerals.
241
+
242
+ Queue overview cards use a compact layout capped at 460 px, with the Queue name,
243
+ total Task count, colored status counts, and a thin proportional status distribution
244
+ bar. Status controls retain navigation to the selected status and wrap on narrow
245
+ screens. They have padded hover targets and offset keyboard focus outlines. The
246
+ cards show completed/total Tasks and completion percentage below the bar, and compact
247
+ Worker and Route summaries in the footer. Status numbers use five aligned columns.
248
+ Route counts are distinct routes, classified Busy before Idle before Waiting; Waiting
249
+ means pending/running Task demand without active Workers. Inactive routes are omitted. Worker counts come from paginated Worker
250
+ groups, refreshed every 15 seconds; loading or unavailable observations never display
251
+ as zero. Completion includes succeeded, failed and cancelled Tasks. Failure rate and
252
+ recent Task details are omitted; individual Tasks remain available inside the Queue.
@@ -0,0 +1,14 @@
1
+ # v0.2.0
2
+
3
+ - Add Routes navigation and idle/busy Worker observations, with grouped Queue progress counts. These features require Server 2.2 or later and observation-capable Workers.
4
+ - Add case-insensitive fuzzy Task name search through the Server.
5
+ - Add dismissible warnings when the connected Server is older than the installed Client.
6
+ - Change transient errors to corner notifications with duplicate suppression, dismissal and independent retry recovery.
7
+ - Fix status counts to preserve the applied name and filter when switching Task status.
8
+ - Fix Task drawer navigation, browser history and focus when switching between Tasks or interacting with notifications.
9
+ - Fix keyboard navigation and dismissal for dropdowns and saved-view menus, including WebKit.
10
+ - Fix selection preservation during sorting and saved-view comparison, Worker pagination recovery and long notification wrapping.
11
+
12
+ Validated against Labtasker 2.2.0 using both Client 2.1.0 and Client 2.2.0. No connection or saved-view migration is required.
13
+
14
+ Full Changelog: https://github.com/luocfprime/labtasker-webui/compare/v0.1.1...v0.2.0
@@ -0,0 +1,6 @@
1
+ # v0.2.1
2
+
3
+ - Add status-aware Cancel, Requeue and Delete actions for selected Tasks, with disabled-state guidance and partial-failure recovery.
4
+ - Fix Task selection controls so checkboxes remain visible while horizontally scrolling wide tables.
5
+
6
+ Full Changelog: https://github.com/luocfprime/labtasker-webui/compare/v0.2.0...v0.2.1
@@ -0,0 +1 @@
1
+ :root{--font-ui:-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;--font-code:ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;--text-small:11px;--text-body:12px;--text-title:22px;--header-height:40px;--control-height:32px;font-family:var(--font-ui);font-size:var(--text-body);color:#202631;font-synthesis:none;--blue:#1769d2;--line:#dfe3e8;--muted:#687385;--panel:#fff;--red:#c93636;background:#f8f9fb;font-weight:400;line-height:1.5}*{box-sizing:border-box}#toast{overflow-wrap:anywhere;color:#344054;max-width:min(420px,100vw - 24px);font-size:var(--text-body);opacity:0;pointer-events:none;background:#fff;border:1px solid #cbd8e8;border-radius:7px;padding:11px 14px;transition:all .16s;position:relative;transform:translateY(8px);box-shadow:0 10px 30px #20395624}#toast:not(.visible){border:0;width:0;height:0;padding:0;position:absolute;overflow:hidden}#toast.visible{opacity:1;transform:none}body{min-width:320px;margin:0}button,input,select{font:inherit;letter-spacing:inherit}input,select{font-weight:400}h1,h2,h3,strong,b{font-weight:600}button{cursor:pointer;white-space:nowrap}:where(button,input,select,summary,a,[tabindex]):focus-visible{outline:2px solid var(--blue);outline-offset:3px}.top{height:var(--header-height);border-bottom:1px solid var(--line);z-index:8;background:#fff;justify-content:space-between;align-items:center;padding:0 20px;display:flex;position:sticky;top:0}.brand{align-items:center;gap:8px;font-size:17px;line-height:24px;display:flex}.logo{object-fit:contain;flex-shrink:0;width:24px;height:24px;display:block}.server{font-size:var(--text-body);color:var(--muted);align-items:center;gap:8px;display:flex}.live{background:#2c9a67;border-radius:50%;width:8px;height:8px}.page,.workspace{max-width:none;margin:auto;padding:14px 20px 16px}.page-head,.workspace-head{justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:18px;display:flex}.eyebrow{letter-spacing:.13em;color:#6d788a;font-size:11px;font-weight:600}.page h1,.workspace h1{font-size:var(--text-title);letter-spacing:-.025em;margin:5px 0 4px;font-weight:600;line-height:1.25}.page-head p,.workspace-head p{color:var(--muted);font-size:var(--text-body);margin:0}.primary,.secondary,.toolbar button,.pager button,.actions button,.modal-actions button{color:#303846;min-height:var(--control-height);font-weight:500;font-size:var(--text-body);background:#fff;border:1px solid #cdd3da;border-radius:6px;padding:0 14px;line-height:1.25}.primary,.toolbar button.primary{background:var(--blue);border-color:var(--blue);color:#fff}.toolbar button.danger-link:not(:disabled){color:var(--red)}.secondary{flex-shrink:0;align-items:center;gap:8px;display:flex}.compact{min-height:var(--control-height)}.link,.danger-link{color:var(--blue);font-size:var(--text-body);background:0 0;border:0;padding:3px}.danger-link{color:var(--red)}button:disabled{opacity:.45;cursor:default}.spinner{border:2px solid;border-right-color:#0000;border-radius:50%;width:14px;height:14px;animation:.8s linear infinite spin;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}.loading{text-align:center;color:var(--muted);justify-content:center;gap:10px;padding:50px;display:flex}.queue-grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,430px),1fr));gap:18px;display:grid}.queue-card{border:1px solid var(--line);cursor:pointer;background:#fff;border-radius:8px;padding:16px;transition:all .15s}.queue-card:hover,.queue-card:focus{border-color:#94b9e8;box-shadow:0 4px 18px #182a4410}.queue-title{justify-content:space-between;align-items:flex-start;margin-bottom:14px;display:flex}.queue-title h2{letter-spacing:-.015em;margin:0 0 3px;font-size:18px;line-height:1.35}.queue-title span{color:var(--muted);font-size:var(--text-small)}.queue-title .arrow{color:#1769d2;font-size:19px}.status-row{border:1px solid #e4e7eb;border-radius:6px;grid-template-columns:repeat(5,1fr);display:grid;overflow:hidden}.status-row button{text-align:left;background:#fbfcfd;border:0;border-right:1px solid #e4e7eb;padding:9px 8px}.status-row button:last-child{border:0}.status-row i{border-radius:50%;width:6px;height:6px;margin-right:5px;display:inline-block}.status-row b{font-size:var(--text-body)}.status-row span{color:var(--muted);margin:3px 0 0 11px;font-size:11px;display:block}.pending{--state:#c18a18}.running{--state:#1776df}.succeeded{--state:#23855b}.failed{--state:#d04545}.cancelled{--state:#87919f}.status-row i,.badge i{background:var(--state)}.progress{background:#edf0f3;border-radius:5px;height:5px;margin:18px 0 12px;overflow:hidden}.progress span{background:#4b98e9;height:100%;display:block}.queue-foot{font-size:var(--text-small);color:var(--muted);flex-direction:column;align-items:stretch;gap:12px;min-height:25px;display:flex}.queue-metrics{font-variant-numeric:tabular-nums;justify-content:space-between;gap:14px;display:flex}.recent-task{text-overflow:ellipsis;white-space:nowrap;max-width:100%;overflow:hidden}.badge{color:#3e4755;text-transform:capitalize;white-space:nowrap;background:#f2f4f6;border-radius:12px;align-items:center;gap:6px;padding:3px 8px;font-size:11px;display:inline-flex}.badge i{border-radius:50%;width:6px;height:6px}.duration{white-space:nowrap;font-variant-numeric:tabular-nums;align-items:center;gap:5px;display:inline-flex}.duration i{background:#2c9a67;border-radius:50%;width:6px;height:6px}.empty{text-align:center;color:var(--muted);padding:70px 20px}.empty h2{color:#333b47;margin:10px 0 4px;font-size:18px}.empty p{font-size:var(--text-body);margin:0}.empty-icon{color:#9aa3af;font-size:28px}.connect{background:linear-gradient(115deg,#f6f9fd,#fff);place-items:center;min-height:100vh;display:grid}.connect-card{border:1px solid var(--line);background:#fff;border-radius:10px;width:min(450px,100vw - 32px);padding:32px;box-shadow:0 18px 50px #1d365318}.connect-copy{margin:40px 0 25px}.connect-copy h1{font-size:var(--text-title);letter-spacing:-.025em;margin:7px 0;line-height:1.25}.connect-copy p,.privacy{color:var(--muted);font-size:var(--text-body);line-height:1.55}.connect form label{font-size:var(--text-body);margin:16px 0;font-weight:500;display:block}.connect label span{float:right;color:#8b94a2;font-weight:400}.connect input{font-size:var(--text-body);border:1px solid #ccd3dc;border-radius:6px;width:100%;height:40px;margin-top:7px;padding:0 11px;font-weight:400;display:block}.connect form .primary{width:100%;margin-top:8px}.privacy{text-align:center;margin:20px 20px 0}.error,.stale{color:#9b3030;font-size:var(--text-small);background:#fff1f1;border:1px solid #f0cccc;border-radius:5px;margin:10px 0;padding:10px 12px}.field-error{color:#a23838;font-size:var(--text-small);margin:-6px 10px 12px}.stale{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.stale>div{overflow-wrap:anywhere;flex:280px;min-width:0}.stale button{color:inherit;background:0 0;border:1px solid;border-radius:5px;flex:none;padding:5px 9px}.stale{color:#7f651a;background:#fff8e5;border-color:#eadca7}.splash{place-content:center;justify-items:center;gap:25px;height:100vh;display:grid}.crumb{font-size:var(--text-body);color:var(--muted);align-items:center;gap:10px;margin-bottom:8px;display:flex}.crumb button{color:var(--blue);background:0 0;border:0;padding:0}.workspace-head{margin-bottom:12px}.stats{border:1px solid var(--line);background:#fff;border-radius:7px;grid-template-columns:repeat(5,1fr);margin-bottom:14px;display:grid}.stats button{border:0;border-right:1px solid var(--line);text-align:left;background:0 0;padding:8px 12px}.stats button:last-child{border:0}.stats span{color:var(--muted);text-transform:capitalize;font-size:11px}.stats strong{font-variant-numeric:tabular-nums;margin-top:2px;font-size:18px;line-height:1.3;display:block}.toolbar{border:1px solid var(--line);background:#fff;border-radius:7px;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:12px;padding:10px;display:flex}.toolbar input:not([type=checkbox]),.toolbar select{height:var(--control-height);font-size:var(--text-body);background:#fff;border:1px solid #ccd3dc;border-radius:5px;padding:0 10px}.toolbar .filter{flex:180px;min-width:0}.column-menu{flex:none;position:relative}.column-menu summary{height:var(--control-height);color:#303846;font-size:var(--text-body);cursor:pointer;border:1px solid #ccd3dc;border-radius:5px;align-items:center;padding:0 10px;list-style:none;display:flex}.column-menu>div{z-index:6;border:1px solid var(--line);background:#fff;border-radius:6px;width:176px;max-height:min(280px,40dvh);padding:4px;position:fixed;top:calc(100% + 6px);left:0;overflow-y:auto;box-shadow:0 8px 22px #1d36531c}.column-menu summary{gap:12px}.column-menu summary::-webkit-details-marker{display:none}.column-menu summary:after{content:"";border-bottom:1.5px solid;border-right:1.5px solid;width:6px;height:6px;transform:translateY(-2px)rotate(45deg)}.column-menu[open] summary{background:#eceef1}.column-menu input[type=checkbox]{width:14px;height:14px;accent-color:var(--blue);flex:0 0 14px;margin:0;padding:0}.column-menu label:hover,.column-menu label:focus-within{background:#f3f5f7}.column-menu label{cursor:pointer;color:#303846;min-height:32px;font-size:var(--text-body);border-radius:3px;align-items:center;gap:10px;padding:6px 10px;display:flex}.selection{min-height:45px;font-size:var(--text-body);background:#f0f7ff;border:1px solid #a9caef;border-radius:6px;align-items:center;gap:10px;margin-bottom:10px;padding:0 14px;display:flex}.selection-summary{flex-direction:column;min-width:0;display:flex}.selection-summary small{color:var(--red);overflow-wrap:anywhere}.selection-actions{flex-wrap:wrap;align-items:center;gap:8px;margin-left:auto;display:flex}.selection-action-hint{display:inline-flex}.table-wrap{border:1px solid var(--line);background:#fff;border-radius:7px;min-height:260px;overflow:auto}table{border-collapse:collapse;width:100%;line-height:1.35;font-size:var(--text-body)}th{text-align:left;color:#657083;border-bottom:1px solid var(--line);white-space:nowrap;background:#f7f8fa;padding:8px 10px;font-weight:500}td{color:#4c5665;white-space:nowrap;border-bottom:1px solid #edf0f2;padding:5px 10px}tbody tr{cursor:pointer}tbody tr:hover,tbody tr.chosen{background:#f4f8fd}td b{color:#2b3440;font-size:var(--text-body);margin-bottom:1px;display:block}td code{display:block}td code,.drawer code{color:var(--muted);font-family:var(--font-code);font-size:11px;font-weight:400;line-height:1.5}.chips{gap:3px;display:flex}.chips span{background:#f8f9fa;border:1px solid #dce1e7;border-radius:4px;padding:2px 5px}.pager{color:var(--muted);font-size:var(--text-small);justify-content:space-between;align-items:center;margin-top:13px;display:flex}.pager button{margin-left:7px}.drawer{right:0;top:var(--header-height);z-index:7;background:#fff;border-left:1px solid #ccd3da;flex-direction:column;width:min(600px,100vw);display:flex;position:fixed;bottom:0;box-shadow:-12px 0 35px #253b581a}.drawer-resize{z-index:2;cursor:col-resize;outline:none;width:10px;position:absolute;inset:0 auto 0 -5px}.drawer-resize:hover,.drawer-resize:focus-visible{border-left:2px solid var(--blue)}.drawer-head{border-bottom:1px solid var(--line);justify-content:space-between;padding:20px 24px;display:flex}.drawer-head h2{letter-spacing:-.015em;margin:5px 0 3px;font-size:20px;line-height:1.3}.close{color:#536071;background:#f2f4f6;border:0;border-radius:5px;width:32px;height:32px;font-size:23px}.drawer-body{padding:20px 24px 60px;overflow:auto}.task-summary{justify-content:space-between;align-items:center;margin-bottom:15px;display:flex}.actions{gap:7px;display:flex}.detail-section{border-top:1px solid var(--line);padding:18px 0}.detail-section h3{font-size:var(--text-body);margin:0 0 13px}.detail-section dl{font-size:var(--text-body);grid-template-columns:110px 1fr;gap:9px;margin:0;display:grid}.detail-section dt{color:var(--muted)}.detail-section dd{margin:0}.section-title{align-items:center;display:flex}.section-title h3{margin-right:auto}.detail-section pre{max-height:300px;font:12px/1.6 var(--font-code);background:#f5f7f9;border:1px solid #e4e7eb;border-radius:6px;padding:13px;overflow:auto}.json-tree{max-height:420px;font:12px/1.6 var(--font-code);background:#f8fafc;border:1px solid #e4e7eb;border-radius:6px;padding:10px 12px;overflow:auto}.json-node summary{cursor:pointer;color:#4b5665;list-style-position:outside}.json-node>.json-copy,.json-leaf button{visibility:hidden;color:var(--blue);font:inherit;background:0 0;border:0;margin-left:8px;padding:0}.json-node:hover>.json-copy,.json-node:focus-within>.json-copy,.json-leaf:hover button,.json-leaf:focus-within button{visibility:visible}.json-node{position:relative}.json-node>.json-copy{position:absolute;top:0;right:0}.json-children{border-left:1px solid #dce2e9;margin-left:7px;padding-left:16px}.json-key{color:#7652a2}.json-key:after{content:":"}.json-kind,.json-empty,.json-null{color:#87919f}.json-string{color:#27704e;overflow-wrap:anywhere}.json-scalar{color:#1769d2}.json-leaf{min-height:20px}.json-leaf a{color:var(--blue);overflow-wrap:anywhere}.failure{color:#9f3333}.modal-back{z-index:20;background:#26314066;place-items:center;display:grid;position:fixed;inset:0}.modal{z-index:21;background:#fff;border-radius:9px;width:min(460px,100vw - 32px);max-height:calc(100dvh - 32px);padding:26px;position:fixed;top:50%;left:50%;overflow:auto;transform:translate(-50%,-50%);box-shadow:0 24px 70px #1119}.batch-modal{width:min(620px,100vw - 32px)}.delete-scope{border:1px solid var(--line);font-size:var(--text-small);background:#f8f9fb;border-radius:6px;grid-template-columns:78px 1fr;gap:7px 12px;margin:18px 0;padding:12px;display:grid}.delete-scope dt{color:var(--muted)}.delete-scope dd{overflow-wrap:anywhere;margin:0}.operation-counts{color:var(--muted);gap:18px;display:flex}.failure-list{max-height:180px;overflow:auto}.failure-list .error{grid-template-columns:150px 1fr;gap:8px;display:grid}.modal h2{margin:8px 0;font-size:20px}.modal p{font-size:var(--text-body);color:var(--muted);line-height:1.55}.danger-mark{width:34px;height:var(--control-height);color:var(--red);background:#fff0f0;border-radius:50%;place-items:center;font-weight:700;display:grid}.modal-actions{justify-content:flex-end;gap:8px;margin-top:22px;display:flex}.danger{color:#fff!important;background:#c93636!important;border-color:#c93636!important}.operation{margin-top:20px}.operation>div:first-child{font-size:var(--text-small);justify-content:space-between;display:flex}.operation .progress{margin-top:8px}@media (width<=900px){body{min-width:0}.page,.workspace{padding:16px 18px}.queue-grid{grid-template-columns:1fr}.drawer{z-index:10;top:0;width:100%!important}.drawer-resize{display:none}.top{padding:0 18px}.status-row,.stats{grid-template-columns:repeat(5,minmax(65px,1fr));overflow:auto}.toolbar{flex-wrap:wrap}.toolbar .filter{flex-basis:100%}.table-wrap{overflow:auto}}@media (width<=600px){.top{height:auto;min-height:var(--header-height);flex-wrap:wrap;gap:10px;padding:8px 18px}.server{overflow-wrap:anywhere;flex-wrap:wrap;min-width:0}.live{flex:0 0 8px}.queue-card{min-width:0;padding:16px}.status-row,.stats{grid-template-columns:repeat(5,minmax(0,1fr))}.status-row button,.stats button{text-align:center;min-width:0;padding:10px 4px}.status-row span{margin-left:0}.stats span{font-size:11px}.toolbar{grid-template-columns:repeat(2,minmax(0,1fr));display:grid}.toolbar>*{width:100%;min-width:0}.toolbar .filter{grid-column:1/-1}.column-menu summary{justify-content:center}.column-menu>div{left:auto;right:0}.pager,.selection,.task-summary{flex-wrap:wrap;gap:12px;height:auto}.selection{padding:12px}.pager>div{margin-left:auto}.drawer-head,.drawer-body{padding-left:18px;padding-right:18px}.drawer-head>div{overflow-wrap:anywhere;min-width:0}.close{flex-shrink:0}.detail-section dd{overflow-wrap:anywhere;min-width:0}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important}}.workspace{height:calc(100dvh - var(--header-height));flex-direction:column;min-height:560px;display:flex}.workspace>*{flex-shrink:0}.workspace .table-wrap{flex:auto;min-height:180px}.table-wrap th{z-index:1;position:sticky;top:0}.task-name{text-overflow:ellipsis;max-width:clamp(240px,26vw,520px);overflow:hidden}@media (width<=900px){.workspace{height:auto;min-height:calc(100dvh - var(--header-height))}.workspace .table-wrap{flex:none;max-height:65dvh}}.list-summary{color:var(--muted);font-size:var(--text-small);margin-top:8px}.load-more{min-height:1px;color:var(--muted);font-size:var(--text-small);justify-content:center;align-items:center;gap:8px;display:flex}.load-more:not(:empty){padding:12px}.toolbar>input,.toolbar>select,.toolbar>button,.column-menu>summary{box-sizing:border-box;height:var(--control-height);min-height:var(--control-height);font-family:var(--font-ui);font-size:var(--text-body);vertical-align:middle;border:1px solid #ccd3dc;border-radius:5px;margin:0;padding:0 10px;line-height:18px}.toolbar>button,.column-menu>summary{justify-content:center;align-items:center;display:inline-flex}.toolbar>select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='m3 4.5 3 3 3-3' fill='none' stroke='%23515c6c' stroke-width='1.5'/%3E%3C/svg%3E");background-position:right 9px center;background-repeat:no-repeat;padding-right:28px}.column-menu>summary:after{width:5px;height:5px}td{line-height:16px}td code{line-height:14px}@media (pointer:coarse){.toolbar{--control-height:36px}.column-menu label{min-height:36px}}.virtual-table{table-layout:fixed}.virtual-table tbody tr:not(.virtual-spacer){height:42px}.virtual-table td{text-overflow:ellipsis;overflow:hidden}.virtual-table th[data-column-id=select],.virtual-table td[data-column-id=select]{box-shadow:1px 0 var(--line);position:sticky;left:0}.virtual-table th[data-column-id=select]{z-index:3}.virtual-table td[data-column-id=select]{z-index:1;background:#fff}.virtual-table tbody tr:hover td[data-column-id=select],.virtual-table tbody tr.chosen td[data-column-id=select]{background:#f4f8fd}.virtual-table .task-name{max-width:100%}.virtual-table .chips{overflow:hidden}.virtual-table td code{text-overflow:ellipsis;overflow:hidden}.virtual-spacer td{border:0;padding:0}.column-resizer{cursor:col-resize;touch-action:none;z-index:2;width:7px;position:absolute;inset:0 0 0 auto}.column-resizer:hover,.column-resizer:focus-visible{background:#1769d244}.instant-tooltip{z-index:10000;pointer-events:none;color:#263140;white-space:pre-wrap;overflow-wrap:anywhere;background:#fff;border:1px solid #d5dbe3;border-radius:5px;max-width:min(640px,100vw - 16px);max-height:calc(100vh - 16px);padding:7px 10px;font:12px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;position:fixed;overflow:hidden;box-shadow:0 3px 12px #18223026}.priority-value{white-space:nowrap;font-variant-numeric:tabular-nums;align-items:baseline;gap:4px;display:inline-flex}.priority-direction{color:#657083}.virtual-table .table-filler{border-left:0;border-right:0;padding:0}.ui-select{flex:none;position:relative}.toolbar .ui-select>button{width:100%;height:var(--control-height);min-height:var(--control-height);font:inherit;color:#202631;background:#fff;border:1px solid #ccd4df;border-radius:5px;justify-content:space-between;align-items:center;gap:14px;padding:0 10px;font-weight:400;display:flex}.ui-select-menu{z-index:100;border:1px solid var(--line);width:max-content;min-width:100%;font-size:var(--text-body);background:#fff;border-radius:5px;padding:4px;position:absolute;top:calc(100% + 4px);left:0;box-shadow:0 3px 12px #18223020}.ui-select-menu [role=option]{cursor:pointer;border-radius:3px;justify-content:space-between;align-items:center;gap:20px;min-height:28px;padding:4px 8px;display:flex}.ui-select-menu [role=option].highlighted{background:#edf3fb}.ui-select-menu [aria-selected=true]{color:#1769cc}.priority-direction.positive{color:#c93636}.priority-direction.negative{color:#2275c9}.filter-input{position:relative}.toolbar .filter-input>input{width:100%;padding-right:32px}.toolbar .filter-help-button{color:#687385;background:0 0;border:0;border-radius:4px;width:22px;height:22px;min-height:0;padding:0;font-size:12px;position:absolute;top:5px;right:5px}.toolbar .filter-help-button:hover,.toolbar .filter-help-button[aria-expanded=true]{color:#1769d2;background:#edf3fb}.filter-help{z-index:110;border:1px solid var(--line);background:#fff;border-radius:6px;width:min(460px,100vw - 48px);padding:14px;font-size:12px;line-height:1.5;position:absolute;top:calc(100% + 6px);left:0;box-shadow:0 4px 16px #18223020}.filter-help p{margin:8px 0 0}.filter-help code{font-size:11px}.filter-examples{gap:3px;margin:10px 0;display:grid}.toolbar .filter-examples button{text-align:left;background:#f8f9fb;border:0;border-radius:3px;flex-wrap:wrap;justify-content:space-between;gap:4px 12px;width:100%;height:auto;min-height:30px;padding:5px 7px;font-size:12px;font-weight:400;display:flex}.toolbar .filter-examples button:hover{background:#edf3fb}.column-menu>div{width:370px;max-width:calc(100vw - 24px);max-height:min(520px,65dvh)}.column-list{max-height:360px;overflow-y:auto}.column-choice{align-items:center;gap:3px;display:flex}.column-menu .column-choice label{flex:1;min-width:0;padding:5px 6px}.column-choice label span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.toolbar .column-choice button{background:0 0;border:0;width:24px;height:26px;min-height:0;padding:0}.custom-column-form{border-top:1px solid var(--line);margin-top:6px;padding:10px 6px 4px}.column-menu .custom-column-form label{padding:0 0 6px}.custom-column-form>div{gap:6px;display:flex}.toolbar .custom-column-form input{width:100%;min-width:0}.custom-column-form small{color:var(--muted);margin-top:6px;display:block}.column-drag-handle{cursor:grab;color:var(--muted);touch-action:none}.column-drag-handle:active{cursor:grabbing}.column-choice.drop-target{box-shadow:inset 0 2px var(--blue);background:0 0}.column-menu .column-choice.drop-target label{background:0 0}.virtual-table th{-webkit-user-select:none;user-select:none}.filter-help{max-height:min(650px,70vh);overflow-y:auto}.save-filter,.saved-filter-row{gap:6px;display:flex}.toolbar .save-filter input{flex:1;width:0;min-width:0}.toolbar .saved-filter-row>button:last-child{flex:none;width:26px}.queue-navigation{justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.queue-navigation .crumb{min-width:0;margin:0}.queue-navigation .crumb b{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.connection-mode{border:1px solid var(--line);border-radius:4px;padding:0 6px;font-size:11px;line-height:20px}.connection-address{text-overflow:ellipsis;white-space:nowrap;max-width:min(50vw,650px);overflow:hidden}.queue-navigation-actions,.views{align-items:center;gap:6px;display:flex}.views{position:relative}.views .ui-select{width:180px}.views button,.views input{border:1px solid var(--line);color:#303846;background:#fff;border-radius:5px;min-height:32px;padding:0 10px}.views .ui-select>button{justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.views .ui-select>button span{text-overflow:ellipsis;overflow:hidden}.views-menu{border:1px solid var(--line);z-index:40;background:#fff;border-radius:6px;width:180px;padding:12px;position:absolute;top:calc(100% + 6px);right:0;box-shadow:0 4px 16px #15223814}.views-menu p{color:var(--muted);margin:6px 0 10px;font-size:12px}.views-menu input{box-sizing:border-box;width:100%}.view-actions{grid-template-columns:1fr 1fr;gap:6px;margin-top:8px;display:grid}@media (width<=600px){.queue-navigation{flex-wrap:wrap}.views-menu{width:min(180px,80vw);left:0;right:auto}}.connect .ui-select>button{border:1px solid var(--line);background:#fff;border-radius:5px;justify-content:space-between;align-items:center;width:100%;min-height:32px;padding:0 10px;display:flex}.connect label small{color:var(--muted);margin-top:6px;font-weight:400;display:block}.view-selector{position:relative}.select-value{align-items:center;gap:6px;min-width:0;display:inline-flex}.select-value-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.ui-select>button>svg{flex-shrink:0}.view-dirty{background:var(--blue);border-radius:50%;flex:0 0 6px;width:6px;height:6px;display:inline-block}.views .view-save{color:var(--blue);border-color:#a6c6ed}.views .select-action{border:0;border-top:1px solid var(--line);text-align:left;width:100%;color:var(--blue);border-radius:0;margin-top:4px;padding:6px 8px;display:block}.views-menu{width:180px;padding:4px}.views-menu button{text-align:left;border:0;border-radius:3px;width:100%;font-weight:400;display:block}.views-menu button:not(:disabled):hover,.views-menu button:focus-visible{background:#edf3fb}.views-menu [role=separator]{border-top:1px solid var(--line);margin:4px}.views-menu .view-delete{color:var(--red)}.view-dialog{width:min(380px,100vw - 32px);padding:20px}.view-dialog h2{overflow-wrap:anywhere;font-size:17px}.view-dialog label{margin:16px 0 6px;display:block}.view-dialog input{border:1px solid var(--line);border-radius:5px;width:100%;height:34px;padding:0 10px}.view-dialog .view-name-error{color:var(--red);margin:6px 0}.stats .pending{--count-color:#956600}.stats .running{--count-color:#1768bd}.stats .succeeded{--count-color:#23855b}.stats .failed{--count-color:#c93636}.stats .cancelled{--count-color:#687385}.stats span:before{content:"";background:var(--state);vertical-align:1px;border-radius:50%;width:6px;height:6px;margin-right:6px;display:inline-block}.stats strong{color:var(--count-color)}.stats button:hover{background:color-mix(in srgb, var(--state) 5%, white)}.stats button.active{box-shadow:inset 0 -2px var(--state);background:color-mix(in srgb, var(--state) 9%, white)}.profile-save-error{pointer-events:auto;color:#624719;background:#fff9ed;border:1px solid #e3ba6c;border-radius:6px;align-items:center;gap:12px;width:min(420px,100vw - 24px);padding:10px 12px;font-size:13px;display:flex}.ui-select-menu,.filter-help,.views-menu{overscroll-behavior:contain;overflow-wrap:anywhere;position:fixed;overflow-y:auto}.ui-select-menu [role=option]>span:first-child{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.ui-select-menu [role=option]>span:last-child{flex-shrink:0}.server-version-warning{color:#674d0a;background:#fff8e6;border:1px solid #d7b665;border-radius:6px;align-items:baseline;gap:12px;margin:12px 0;padding:10px 14px;font-size:13px;display:flex}.server-version-warning span{overflow-wrap:anywhere;min-width:0}.server-version-warning button{color:inherit;flex-shrink:0}[hidden]{display:none!important}.queue-workspace{flex:1;grid-template-columns:200px minmax(0,1fr);gap:18px;min-height:0;display:grid}.queue-workspace.routes-collapsed{grid-template-columns:minmax(0,1fr)}.workspace-content,.task-workspace,.workers-panel{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.route-sidebar{border-right:1px solid var(--line);flex-direction:column;min-height:0;padding-right:12px;display:flex}.route-sidebar-head{justify-content:space-between;align-items:center;height:34px;display:flex}.route-list{flex:1;min-height:0;overflow:auto}.route-item{text-align:left;background:0 0;border:0;flex-direction:column;gap:4px;width:100%;margin-bottom:3px;padding:9px 10px;display:flex}.route-item>span,.active-route{text-overflow:ellipsis;white-space:nowrap;max-width:100%;overflow:hidden}.route-item small{color:var(--muted);font-size:11px}.route-item.active{color:#245987;background:#edf3fa}.inactive-routes{align-items:center;gap:6px;padding:12px 0;font-size:11px;display:flex}.workspace-tabs{border-bottom:1px solid var(--line);align-items:center;gap:6px;margin-bottom:12px;padding-bottom:7px;display:flex}.workspace-tabs button{background:0 0;border:0}.workspace-tabs button[aria-pressed=true]{color:#245987;background:#eaf0f7}.active-route{color:var(--muted);margin-left:8px}.stats,.worker-stats{background:0 0;border:0;flex-wrap:wrap;gap:6px 18px;margin-bottom:12px;display:flex}.stats button,.worker-stats button{text-align:left;background:0 0;border:0;align-items:baseline;gap:8px;padding:5px 7px;display:inline-flex}.stats strong,.worker-stats strong{font-variant-numeric:tabular-nums;margin:0;font-size:27px;font-weight:600;line-height:1.25;display:inline}.stats span,.worker-stats button{color:var(--muted);font-size:12px}.stats span:before{content:none}.worker-stats strong{color:#28313c}.worker-stats button[aria-pressed=true]{background:#edf3fa}.worker-toolbar{color:var(--muted);align-items:center;gap:12px;margin-bottom:10px;display:flex}.worker-table table{table-layout:fixed;width:100%}.worker-table th:first-child{width:28%}.worker-table td>code,.worker-route,.worker-table td>.link{text-overflow:ellipsis;white-space:nowrap;max-width:100%;display:block;overflow:hidden}.worker-table td{padding:10px}.worker-table td small{white-space:normal;overflow-wrap:anywhere;margin-top:4px;display:block}.worker-idle{color:#23855b;background:#edf8f1}.worker-busy{color:#1768bd;background:#edf4ff}.observation-delayed{color:#956600}@media (width<=900px){.queue-workspace{grid-template-columns:160px minmax(0,1fr);gap:10px}.stats,.worker-stats{gap:4px}.stats strong,.worker-stats strong{font-size:23px}.route-list{max-height:65dvh}}@media (width<=600px){.queue-workspace{grid-template-columns:minmax(0,1fr)}.route-sidebar{border-right:0;border-bottom:1px solid var(--line);padding:0}.route-list{max-height:160px}}:root{--text-body:13px;color:#242a34;--blue:#245cc7;--muted:#626d7c;--line:#e3e7ec;background:#fff}.task-name{font-weight:500}.queue-workspace{grid-template-columns:200px minmax(0,1fr);transition:grid-template-columns .14s,gap .14s}.queue-workspace.routes-collapsed{grid-template-columns:0 minmax(0,1fr);gap:0}.route-rail{min-width:0;min-height:0;display:flex;overflow:hidden}.routes-collapsed .route-rail{visibility:hidden}.route-collapse{border:1px solid var(--line);cursor:pointer;background:#fff;border-radius:5px;justify-content:center;align-items:center;width:28px;height:28px;font-size:20px;display:inline-flex}.route-item{border:1px solid #0000;border-radius:6px;transition:background .12s,border-color .12s}.route-item:hover{background:#f5f7fa}.route-item.active{color:#245cc7;background:#edf3ff;border-color:#b7c9ed}.route-name{align-items:center;gap:7px;display:flex}.route-dot{vertical-align:1px;background:#9aa3af;border-radius:50%;flex:0 0 6px;width:6px;height:6px;margin-right:5px;display:inline-block}.route-name .route-dot{margin:0}.route-busy{background:#2674d1}.route-idle{background:#23855b}.route-waiting{background:#ab7917}.route-unknown{background:0 0;box-shadow:inset 0 0 0 1px #9aa3af}.workspace-tabs{gap:12px;padding-bottom:0}.workspace-tabs>button{color:#242a34;border-bottom:2px solid #0000;border-radius:0;padding:8px 13px;font-size:14px;transition:color .12s,background .12s}.workspace-tabs>button:hover{background:#f5f7fa}.workspace-tabs>button[aria-pressed=true]{color:#245cc7;background:0 0;border-bottom-color:#245cc7}.workspace-tabs>.secondary{border:1px solid var(--line);border-radius:5px;align-items:center;gap:7px;margin-bottom:5px;margin-right:4px;padding:5px 9px;font-size:12px;display:inline-flex}.stats{gap:8px 18px;padding:2px 0}.stats .all{--state:#626d7c;--count-color:#242a34}.stats button,.worker-stats button{cursor:pointer;border-radius:6px;padding:6px 9px;transition:background .12s,box-shadow .12s}.stats button.active,.worker-stats button[aria-pressed=true]{box-shadow:none;background:#f0f3f7}.worker-toolbar .ui-select>button{height:var(--control-height);color:#242a34;background:#fff;border:1px solid #ccd4df;border-radius:5px;justify-content:space-between;align-items:center;gap:16px;padding:0 10px;display:flex}@media (width<=900px){.queue-workspace{grid-template-columns:160px minmax(0,1fr)}.route-rail .route-sidebar{width:160px;min-width:160px}}@media (width<=600px){.queue-workspace,.queue-workspace.routes-collapsed{grid-template-columns:minmax(0,1fr)}.routes-collapsed .route-rail{display:none}.route-rail .route-sidebar{width:100%;min-width:0}}@media (prefers-reduced-motion:reduce){.queue-workspace,.route-item,.workspace-tabs>button,.stats button,.worker-stats button{transition:none}}.toolbar{background:0 0;border:0;border-radius:0;padding:0}.table-wrap{border:0;border-radius:0}.virtual-table .task-name{color:#245cc7;font-weight:500}.badge{border-radius:4px;padding:2px 7px;font-size:12px}.badge.pending{color:#8b6016;background:#fbf4e5}.badge.running{color:#245cc7;background:#edf3ff}.badge.succeeded{color:#16734b;background:#edf8f2}.badge.failed{color:#ac4242;background:#fceeee}.badge.cancelled{color:#626d7c;background:#f3f5f7}.badge i{display:none}.worker-stats .worker-idle-count strong{color:#23855b}.worker-stats .worker-busy-count strong{color:#2674d1}.queue-workspace{--route-width:clamp(160px, var(--route-preferred-width,200px), 40vw)}.queue-workspace:not(.routes-collapsed){grid-template-columns:var(--route-width) minmax(0, 1fr)}.route-rail{position:relative}.route-rail .route-sidebar{width:var(--route-width);min-width:var(--route-width)}.route-resizer{cursor:col-resize;touch-action:none;z-index:2;width:8px;position:absolute;top:0;bottom:0;right:0}.route-resizer:after{content:"";background:0 0;width:2px;position:absolute;top:0;bottom:0;right:0}.route-resizer:hover:after,.route-resizer:focus-visible:after,.resizing-routes .route-resizer:after{background:var(--blue)}.queue-workspace.resizing-routes{-webkit-user-select:none;user-select:none;cursor:col-resize;transition:none}@media (width<=600px){.queue-workspace:not(.routes-collapsed){grid-template-columns:minmax(0,1fr)}.route-rail .route-sidebar{width:100%;min-width:0}.route-resizer{display:none}}.toolbar .task-name-filter{flex:0 240px;width:240px;min-width:210px;max-width:100%}.queue-navigation{margin-bottom:12px}.crumb,.route-sidebar-head,.workspace-tabs>button{font-size:13px}.crumb b,.route-sidebar-head strong,.workspace-tabs>button{font-weight:500}.route-sidebar-head,.workspace-tabs{border-bottom:1px solid var(--line);height:36px;min-height:36px;margin-bottom:10px}.route-sidebar-head{padding:0 0 0 9px}.workspace-tabs>button{height:36px;padding:6px 12px}.route-item{gap:3px;padding:8px 9px}.route-item.active{background:#edf3ff;border-color:#0000}.stats,.worker-stats{gap:6px 16px}.stats strong,.worker-stats strong{font-size:24px;font-weight:500}.stats span,.worker-stats button{font-size:12px}.stats button,.worker-stats button{padding:6px 8px}.stats button.active{background:color-mix(in srgb, var(--count-color) 9%, white)}.stats button.active span{color:var(--count-color)}.stats .all{--count-color:#242a34}.worker-stats button[aria-pressed=true]{color:#242a34;background:#f0f2f4}.worker-stats .worker-idle-count[aria-pressed=true]{color:#16734b;background:#edf8f2}.worker-stats .worker-busy-count[aria-pressed=true]{color:#245cc7;background:#edf3ff}.virtual-table th,.worker-table th{font-size:12px;font-weight:500}@media (width<=600px){.toolbar .task-name-filter{flex-basis:100%;width:100%;min-width:0}}.queue-location{align-items:center;gap:14px;min-width:0;display:flex}.routes-toggle{height:28px;font-size:var(--text-body);color:var(--muted);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:4px;flex:none;align-items:center;gap:6px;padding:0 8px;font-weight:400;display:inline-flex}.routes-toggle svg{flex-shrink:0;width:18px;height:18px}.routes-toggle:hover,.routes-toggle:focus-visible{color:#242a34;background:#f5f7fa}.routes-toggle[aria-expanded=true]{color:#242a34}.route-all{justify-content:center;height:36px;margin-bottom:10px}.queue-location .crumb{min-width:0}@media (width<=600px){.queue-location{gap:8px;max-width:100%}}.queue-grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,460px),460px))}.queue-card .queue-title{align-items:center;gap:12px;margin-bottom:12px}.queue-title h2{overflow-wrap:anywhere;margin:0;font-size:16px;font-weight:500}.queue-completion{color:var(--muted);flex-shrink:0;font-size:12px}.queue-card .status-row{border:0;border-radius:0;flex-wrap:wrap;justify-content:space-between;gap:4px 0;margin:0 -4px 10px;display:flex;overflow:visible}.queue-card .status-row button{text-align:left;cursor:pointer;background:0 0;border:0;border-radius:5px;align-items:baseline;gap:4px;min-width:0;padding:6px 4px;transition:background .12s;display:inline-flex}.queue-card .status-row button:hover{background:color-mix(in srgb, var(--state) 7%, white)}.queue-card .status-row b{color:var(--state);font-variant-numeric:tabular-nums;font-size:18px;font-weight:500;line-height:1.3}.queue-card .status-row span{text-transform:capitalize;margin:0;font-size:11px;display:inline}.queue-distribution{background:#eef0f3;border-radius:2px;height:4px;display:flex;overflow:hidden}.queue-distribution>span{background:var(--state);flex-shrink:0;height:100%;box-shadow:inset -1px 0 #fff}.queue-card .queue-metrics{justify-content:space-between;margin-top:8px}.queue-card .queue-foot{margin-top:0}.queue-card .status-row button:focus-visible{outline:2px solid var(--state);outline-offset:3px}.queue-card:hover{box-shadow:none;border-color:var(--line)}.queue-card:focus-visible{outline:2px solid var(--blue);outline-offset:3px}.inactive-routes{color:var(--muted);cursor:pointer;justify-content:space-between;gap:10px;padding:10px 8px;font-size:12px;line-height:20px;position:relative}.inactive-routes input{opacity:0;width:1px;height:1px;position:absolute}.inactive-switch{background:#c9cfd8;border-radius:8px;flex:0 0 26px;height:16px;transition:background .12s;position:relative}.inactive-switch:after{content:"";background:#fff;border-radius:50%;width:12px;height:12px;transition:transform .12s;position:absolute;top:2px;left:2px}.inactive-routes input:checked+.inactive-switch{background:var(--blue)}.inactive-routes input:checked+.inactive-switch:after{transform:translate(10px)}.inactive-routes input:focus-visible+.inactive-switch{outline:2px solid var(--blue);outline-offset:3px}@media (prefers-reduced-motion:reduce){.inactive-switch,.inactive-switch:after{transition:none}}.queue-progress-label{color:var(--muted);justify-content:space-between;align-items:baseline;margin:2px 0 7px;font-size:11px;display:flex}.queue-progress-label strong{font-variant-numeric:tabular-nums;font-size:20px;font-weight:500}.queue-worker-summary{color:var(--muted);align-items:baseline;gap:14px;margin-top:12px;font-size:12px;display:flex}.queue-worker-summary b{font-variant-numeric:tabular-nums;font-weight:500}.queue-worker-idle{color:#16734b}.queue-worker-busy{color:#245cc7}.queue-card{padding:20px}.queue-card .queue-title{margin-bottom:20px}.queue-card .status-row{grid-template-columns:repeat(5,minmax(0,1fr));gap:4px;margin:0 -5px 18px;display:grid}.queue-card .status-row button{flex-direction:column;align-items:flex-start;gap:4px;padding:5px;display:flex}.queue-card .status-row b{letter-spacing:-.4px;font-size:24px;line-height:1.2}.queue-card .status-row span{font-size:10px}.queue-progress-label{margin:8px 0 0;font-size:11px}.queue-progress-label strong{font-size:11px}.queue-observation-summary{border-top:1px solid #eff1f4;flex-wrap:wrap;gap:10px 20px;margin-top:18px;padding-top:12px;display:flex}.queue-observation-summary .queue-worker-summary,.queue-route-summary{color:var(--muted);gap:8px;margin:0;font-size:11px;display:flex}.queue-route-summary b{font-weight:500}.queue-route-waiting{color:#ad7a17}@media (width<=380px){.queue-card{padding:16px}.queue-card .status-row b{font-size:21px}.queue-card .status-row{gap:0}}#corner-notifications{max-width:calc(100vw - 24px);max-height:calc(100dvh - var(--header-height) - 24px);overscroll-behavior:contain;z-index:19;pointer-events:none;flex-direction:column;align-items:flex-end;gap:8px;display:flex;position:fixed;bottom:12px;right:12px;overflow:auto}.observation-notices{pointer-events:none;gap:8px;width:min(360px,100vw - 24px);display:grid}.observation-notice{color:#624719;overflow-wrap:anywhere;pointer-events:auto;background:#fff9ed;border:1px solid #e3ba6c;border-radius:8px;padding:12px 14px;box-shadow:0 4px 16px #20395624}.observation-notice p{margin:4px 0 0;font-size:11px}.observation-notice-actions{justify-content:flex-end;gap:16px;margin-top:8px;display:flex}.observation-notice-actions .link{color:inherit}.observation-notices:empty{display:none}#corner-notifications>*{flex-shrink:0}