@linxin666/dsh-client-ui-task-board 0.2.9 → 0.3.0
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.
- package/README.i18n.yaml +2 -2
- package/README.md +1 -0
- package/README.zh.md +1 -0
- package/lib/client.js +18 -159
- package/lib/client.js.map +1 -1
- package/lib/index.js +56 -28
- package/lib/types/core/controller.d.ts +10 -31
- package/lib/types/core/tasks.d.ts +17 -1
- package/lib/types/http.d.ts +42 -0
- package/package.json +1 -1
- package/src/client/board/TaskBoard.tsx +2 -0
- package/src/client/board.module.css +185 -13
- package/src/client/sidebar-entry.ts +2 -2
- package/src/core/controller.ts +16 -152
- package/src/core/tasks.ts +31 -2
- package/src/host-ledger.ts +4 -3
- package/src/host-routes.ts +10 -14
- package/src/host-service.ts +0 -1
- package/src/http.ts +105 -0
- package/lib/types/core/execution.d.ts +0 -197
- package/src/core/execution.ts +0 -425
package/lib/index.js
CHANGED
|
@@ -204,8 +204,19 @@ function resolveDshHome(env = process.env, home = homedir()) {
|
|
|
204
204
|
function dshHome() {
|
|
205
205
|
return resolveDshHome();
|
|
206
206
|
}
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Trim an execution list to at most {@link EXECUTION_HISTORY_LIMIT} records,
|
|
209
|
+
* most recent last. A running (unsettled) execution is never trimmed: the
|
|
210
|
+
* Host monitor and restart recovery depend on the active record, and a task
|
|
211
|
+
* cannot start a new run while one is still open.
|
|
212
|
+
*/
|
|
213
|
+
function retainRecentExecutions(executions) {
|
|
214
|
+
if (executions.length <= 20) return [...executions];
|
|
215
|
+
const open = executions.filter((execution) => execution.endedAt === void 0);
|
|
216
|
+
const settled = executions.filter((execution) => execution.endedAt !== void 0);
|
|
217
|
+
const keepSettled = Math.max(20 - open.length, 0);
|
|
218
|
+
return [...settled.slice(Math.max(settled.length - keepSettled, 0)), ...open];
|
|
219
|
+
}
|
|
209
220
|
/** Statuses a settled task may be archived from. */
|
|
210
221
|
const ARCHIVABLE_STATUSES = ["done", "failed"];
|
|
211
222
|
/** Permission presets a task may pin on its execution session (the `/permission <id>` ids). */
|
|
@@ -307,7 +318,7 @@ function startExecution(task, now, executionId) {
|
|
|
307
318
|
...task,
|
|
308
319
|
status: "running",
|
|
309
320
|
updatedAt: now,
|
|
310
|
-
executions: [...task.executions, execution]
|
|
321
|
+
executions: retainRecentExecutions([...task.executions, execution])
|
|
311
322
|
},
|
|
312
323
|
execution
|
|
313
324
|
};
|
|
@@ -1014,9 +1025,10 @@ function mergeTask(a, b) {
|
|
|
1014
1025
|
const previous = byId.get(entry.id);
|
|
1015
1026
|
byId.set(entry.id, previous === void 0 ? entry : betterExecution(previous, entry));
|
|
1016
1027
|
}
|
|
1028
|
+
const executions = [...byId.values()].sort((x, y) => x.startedAt - y.startedAt);
|
|
1017
1029
|
return {
|
|
1018
1030
|
...newer,
|
|
1019
|
-
executions:
|
|
1031
|
+
executions: retainRecentExecutions(executions)
|
|
1020
1032
|
};
|
|
1021
1033
|
}
|
|
1022
1034
|
function parseHostTasks(values) {
|
|
@@ -1360,7 +1372,10 @@ var HostTaskLedger = class {
|
|
|
1360
1372
|
try {
|
|
1361
1373
|
const parsed = JSON.parse(readFileSync(this.file, "utf8"));
|
|
1362
1374
|
if (parsed.schemaVersion !== 2 || !Array.isArray(parsed.tasks)) throw new Error("unsupported ledger schema");
|
|
1363
|
-
const tasks = parseHostTasks(parsed.tasks)
|
|
1375
|
+
const tasks = parseHostTasks(parsed.tasks).map((task) => ({
|
|
1376
|
+
...task,
|
|
1377
|
+
executions: retainRecentExecutions(task.executions)
|
|
1378
|
+
}));
|
|
1364
1379
|
const invalidScheduleIds = parsed.tasks.flatMap((value) => {
|
|
1365
1380
|
if (typeof value !== "object" || value === null) return [];
|
|
1366
1381
|
const row = value;
|
|
@@ -2194,6 +2209,26 @@ var TaskBoardHostService = class {
|
|
|
2194
2209
|
}
|
|
2195
2210
|
};
|
|
2196
2211
|
//#endregion
|
|
2212
|
+
//#region src/http.ts
|
|
2213
|
+
/** Family-default JSON response headers; callers may append or override. */
|
|
2214
|
+
const JSON_HEADERS = {
|
|
2215
|
+
"content-type": "application/json; charset=utf-8",
|
|
2216
|
+
"referrer-policy": "no-referrer"
|
|
2217
|
+
};
|
|
2218
|
+
/**
|
|
2219
|
+
* Write one JSON response. Default headers are the family defaults
|
|
2220
|
+
* (content-type and referrer-policy); caller headers are appended or
|
|
2221
|
+
* override them.
|
|
2222
|
+
*/
|
|
2223
|
+
function writeJson(res, status, body, headers = {}) {
|
|
2224
|
+
const payload = JSON.stringify(body);
|
|
2225
|
+
res.writeHead(status, {
|
|
2226
|
+
...JSON_HEADERS,
|
|
2227
|
+
...headers
|
|
2228
|
+
});
|
|
2229
|
+
res.end(payload);
|
|
2230
|
+
}
|
|
2231
|
+
//#endregion
|
|
2197
2232
|
//#region src/loopback.ts
|
|
2198
2233
|
/** IPv4 127/8 predicate (four decimal octets, first == 127). */
|
|
2199
2234
|
function isIPv4Loopback(v4) {
|
|
@@ -2245,13 +2280,6 @@ const IMPORT_LIMIT = 2 * 1024 * 1024;
|
|
|
2245
2280
|
const HEARTBEAT_MS = 15e3;
|
|
2246
2281
|
/** Header replaced by an authenticated same-host reverse proxy. */
|
|
2247
2282
|
const TASK_BOARD_PROXY_TOKEN_HEADER = "x-dsh-task-board-proxy-token";
|
|
2248
|
-
function json(res, status, body) {
|
|
2249
|
-
res.writeHead(status, {
|
|
2250
|
-
"content-type": "application/json; charset=utf-8",
|
|
2251
|
-
"cache-control": "no-store"
|
|
2252
|
-
});
|
|
2253
|
-
res.end(JSON.stringify(body));
|
|
2254
|
-
}
|
|
2255
2283
|
function parseAuthority(authority) {
|
|
2256
2284
|
if (authority.trim() !== authority) return void 0;
|
|
2257
2285
|
const match = authority.startsWith("[") ? /^\[[^\]]+\](?::([0-9]+))?$/.exec(authority) : /^[^:@/?#\s]+(?::([0-9]+))?$/.exec(authority);
|
|
@@ -2346,10 +2374,10 @@ function makeTaskBoardRoutes(service, access = {}) {
|
|
|
2346
2374
|
const resolvedAccess = resolveAccess(access);
|
|
2347
2375
|
const guard = (req, res) => {
|
|
2348
2376
|
if (isTrustedTaskBoardRequest(req, resolvedAccess)) return true;
|
|
2349
|
-
|
|
2377
|
+
writeJson(res, 403, {
|
|
2350
2378
|
ok: false,
|
|
2351
2379
|
error: "forbidden"
|
|
2352
|
-
});
|
|
2380
|
+
}, { "cache-control": "no-store" });
|
|
2353
2381
|
return false;
|
|
2354
2382
|
};
|
|
2355
2383
|
return [
|
|
@@ -2357,45 +2385,45 @@ function makeTaskBoardRoutes(service, access = {}) {
|
|
|
2357
2385
|
kind: "exact",
|
|
2358
2386
|
path: `${TASK_BOARD_API_PREFIX}/state`,
|
|
2359
2387
|
handler: (req, res) => {
|
|
2360
|
-
if (req.method !== "GET") return
|
|
2388
|
+
if (req.method !== "GET") return writeJson(res, 405, {
|
|
2361
2389
|
ok: false,
|
|
2362
2390
|
error: "method-not-allowed"
|
|
2363
|
-
});
|
|
2391
|
+
}, { "cache-control": "no-store" });
|
|
2364
2392
|
if (!guard(req, res)) return;
|
|
2365
|
-
|
|
2393
|
+
writeJson(res, 200, service.snapshot(), { "cache-control": "no-store" });
|
|
2366
2394
|
}
|
|
2367
2395
|
},
|
|
2368
2396
|
{
|
|
2369
2397
|
kind: "exact",
|
|
2370
2398
|
path: `${TASK_BOARD_API_PREFIX}/action`,
|
|
2371
2399
|
handler: async (req, res) => {
|
|
2372
|
-
if (req.method !== "POST") return
|
|
2400
|
+
if (req.method !== "POST") return writeJson(res, 405, {
|
|
2373
2401
|
ok: false,
|
|
2374
2402
|
error: "method-not-allowed"
|
|
2375
|
-
});
|
|
2403
|
+
}, { "cache-control": "no-store" });
|
|
2376
2404
|
if (!guard(req, res)) return;
|
|
2377
|
-
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return
|
|
2405
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return writeJson(res, 415, {
|
|
2378
2406
|
ok: false,
|
|
2379
2407
|
error: "json-required"
|
|
2380
|
-
});
|
|
2408
|
+
}, { "cache-control": "no-store" });
|
|
2381
2409
|
try {
|
|
2382
2410
|
const body = await readBody(req);
|
|
2383
2411
|
const parsed = parseActionEnvelope(body.value);
|
|
2384
|
-
if (parsed === void 0) return
|
|
2412
|
+
if (parsed === void 0) return writeJson(res, 400, {
|
|
2385
2413
|
ok: false,
|
|
2386
2414
|
error: "invalid-action"
|
|
2387
|
-
});
|
|
2388
|
-
if (parsed.action.kind !== "import" && Buffer.byteLength(body.raw) > ACTION_LIMIT) return
|
|
2415
|
+
}, { "cache-control": "no-store" });
|
|
2416
|
+
if (parsed.action.kind !== "import" && Buffer.byteLength(body.raw) > ACTION_LIMIT) return writeJson(res, 413, {
|
|
2389
2417
|
ok: false,
|
|
2390
2418
|
error: "body-too-large"
|
|
2391
|
-
});
|
|
2392
|
-
|
|
2419
|
+
}, { "cache-control": "no-store" });
|
|
2420
|
+
writeJson(res, 200, service.apply(parsed.requestId, parsed.action), { "cache-control": "no-store" });
|
|
2393
2421
|
} catch (error) {
|
|
2394
2422
|
const message = error instanceof Error ? error.message : String(error);
|
|
2395
|
-
|
|
2423
|
+
writeJson(res, message === "body-too-large" ? 413 : 400, {
|
|
2396
2424
|
ok: false,
|
|
2397
2425
|
error: message
|
|
2398
|
-
});
|
|
2426
|
+
}, { "cache-control": "no-store" });
|
|
2399
2427
|
}
|
|
2400
2428
|
}
|
|
2401
2429
|
},
|
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
* Board controller: the single owner of task-ledger state and view state.
|
|
3
3
|
*
|
|
4
4
|
* In production it projects the Host ledger and submits confirmed actions;
|
|
5
|
-
* the legacy store
|
|
6
|
-
*
|
|
5
|
+
* the legacy store seam remains for v1 migration tests. It also closes the
|
|
6
|
+
* board whenever the user navigates to a session.
|
|
7
7
|
* Framework-free (structural runtime faces) so the whole orchestration is
|
|
8
8
|
* unit-testable with fakes.
|
|
9
9
|
*
|
|
10
10
|
* The per use-case domain transitions (create/update/delete/schedule) live in
|
|
11
11
|
* dedicated modules under core/use-cases and are applied here; the controller
|
|
12
|
-
* owns only the orchestration seam (state, persistence, notify,
|
|
13
|
-
* navigation, reconciliation).
|
|
12
|
+
* owns only the orchestration seam (state, persistence, notify, navigation).
|
|
14
13
|
*/
|
|
15
|
-
import type { ExecutionService } from './execution.ts';
|
|
16
14
|
import type { TaskStore } from './store.ts';
|
|
17
15
|
import { type NewTaskInput, type TaskRecord, type TaskStatus } from './tasks.ts';
|
|
18
16
|
import { type TaskUpdatePatch } from './use-cases/task-update.ts';
|
|
@@ -37,15 +35,11 @@ export interface SessionsControllerFace {
|
|
|
37
35
|
/** Controller dependencies (all swappable in tests). */
|
|
38
36
|
export interface ControllerDeps {
|
|
39
37
|
store: TaskStore;
|
|
40
|
-
/** Legacy browser execution seam used only by isolated v1 tests. */
|
|
41
|
-
exec?: ExecutionService;
|
|
42
38
|
sessions: SessionsControllerFace;
|
|
43
39
|
/** Clock; defaults to Date.now. */
|
|
44
40
|
now?: () => number;
|
|
45
41
|
/** Id minting; defaults to a random-uuid. */
|
|
46
42
|
uuid?: () => string;
|
|
47
|
-
/** Debounce (ms) for session-list-changed reconciles; defaults to 350. */
|
|
48
|
-
reconcileDebounceMs?: number;
|
|
49
43
|
/** Host-authoritative transport; absent keeps the legacy in-memory test path. */
|
|
50
44
|
transport?: TaskBoardTransport;
|
|
51
45
|
}
|
|
@@ -105,7 +99,7 @@ export declare class BoardController {
|
|
|
105
99
|
private hostState;
|
|
106
100
|
private remoteSubscribed;
|
|
107
101
|
private remoteInitialization;
|
|
108
|
-
/** @param deps - store
|
|
102
|
+
/** @param deps - store and the sessions navigation face. */
|
|
109
103
|
constructor(deps: ControllerDeps);
|
|
110
104
|
/** Load the persisted ledger and start the navigation/status subscriptions. */
|
|
111
105
|
start(): void;
|
|
@@ -178,32 +172,17 @@ export declare class BoardController {
|
|
|
178
172
|
*/
|
|
179
173
|
openSession(sessionId: string): void;
|
|
180
174
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* already running is ignored
|
|
175
|
+
* Request a Host execution for a task: the Host ledger owns the running
|
|
176
|
+
* transition, the execution record, and the settlement. A second call
|
|
177
|
+
* while the task is already running is ignored; without a Host transport
|
|
178
|
+
* the run is refused (returns false).
|
|
184
179
|
*/
|
|
185
180
|
runTask(id: string): Promise<boolean>;
|
|
186
|
-
/** Re-run a settled task
|
|
181
|
+
/** Re-run a settled task through the Host (the Host replans and executes). */
|
|
187
182
|
rerunTask(id: string): Promise<void>;
|
|
188
|
-
|
|
189
|
-
/** Reconcile running tasks and close the board when the user navigates. */
|
|
183
|
+
/** Close the board when the user navigates to another session. */
|
|
190
184
|
private onSessionsChanged;
|
|
191
185
|
private lastCurrent;
|
|
192
|
-
/** Execution ids launched on this page; they settle via their live watch, never list reconciliation. */
|
|
193
|
-
private readonly activeExecutionIds;
|
|
194
|
-
/** Debounce timer for {@link reconcileRunningTasks}. */
|
|
195
|
-
private reconcileTimer;
|
|
196
|
-
/** Whether a reconcile pass is underway (single-flight guard). */
|
|
197
|
-
private reconcileInFlight;
|
|
198
|
-
/**
|
|
199
|
-
* Debounce + single-flight trigger for the running-task reconciliation.
|
|
200
|
-
* Session-list notifications arrive in bursts (one per session status
|
|
201
|
-
* change); both guards together keep a burst from reading the history API
|
|
202
|
-
* once per running task.
|
|
203
|
-
*/
|
|
204
|
-
private scheduleReconcile;
|
|
205
|
-
/** Settle tasks left 'running' whose sessions already finished. */
|
|
206
|
-
private reconcileRunningTasks;
|
|
207
186
|
private persistAndNotify;
|
|
208
187
|
private commitRemote;
|
|
209
188
|
private performRemote;
|
|
@@ -25,6 +25,19 @@ export interface ExecutionRecord {
|
|
|
25
25
|
/** Human failure text when the run failed (prompt rejection or agent error). */
|
|
26
26
|
error: string | undefined;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Maximum number of execution records retained per task. Older settled runs
|
|
30
|
+
* are trimmed when a new execution starts so per-action ledger cost stays
|
|
31
|
+
* bounded regardless of how often a task ran before.
|
|
32
|
+
*/
|
|
33
|
+
export declare const EXECUTION_HISTORY_LIMIT = 20;
|
|
34
|
+
/**
|
|
35
|
+
* Trim an execution list to at most {@link EXECUTION_HISTORY_LIMIT} records,
|
|
36
|
+
* most recent last. A running (unsettled) execution is never trimmed: the
|
|
37
|
+
* Host monitor and restart recovery depend on the active record, and a task
|
|
38
|
+
* cannot start a new run while one is still open.
|
|
39
|
+
*/
|
|
40
|
+
export declare function retainRecentExecutions(executions: readonly ExecutionRecord[]): ExecutionRecord[];
|
|
28
41
|
/**
|
|
29
42
|
* A scheduled-run rule attached to a task. The Host scheduler triggers the
|
|
30
43
|
* task when `nextRunAt` is due and persists the rule in the Host ledger.
|
|
@@ -55,7 +68,10 @@ export interface TaskRecord {
|
|
|
55
68
|
createdAt: number;
|
|
56
69
|
/** Last mutation instant (ms epoch). */
|
|
57
70
|
updatedAt: number;
|
|
58
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Execution history retained on the task, most recent last: the latest
|
|
73
|
+
* {@link EXECUTION_HISTORY_LIMIT} attempts, oldest trimmed on append.
|
|
74
|
+
*/
|
|
59
75
|
executions: ExecutionRecord[];
|
|
60
76
|
/** Optional scheduled-run rule (absent on tasks without a schedule). */
|
|
61
77
|
schedule?: ScheduleRule;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared JSON body/response helpers for the host route families: one strict
|
|
3
|
+
* bounded body reader, one lenient bounded body reader, one JSON object
|
|
4
|
+
* narrow, and one JSON writer. Previously these were copy-pasted across the
|
|
5
|
+
* package route files (routes.ts, update-routes.ts, mobile-api.ts, and each
|
|
6
|
+
* family's route module) with drifting contracts: body caps ranging 4 KiB to
|
|
7
|
+
* 1 MiB and four distinct overflow behaviors (reject, undefined, null, throw).
|
|
8
|
+
*
|
|
9
|
+
* Packages receive this file as a generated copy via scripts/sync-shared.mjs;
|
|
10
|
+
* edit this shared source and re-run the sync instead of editing a copy.
|
|
11
|
+
* Consumer code is migrated onto it in follow-up waves; no call site changes
|
|
12
|
+
* belong in the same change as its introduction.
|
|
13
|
+
* @module dsh-web-ui-shared/host/http
|
|
14
|
+
*/
|
|
15
|
+
import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'node:http';
|
|
16
|
+
/**
|
|
17
|
+
* Strict bounded body reader: parse a request body of at most maxBytes as
|
|
18
|
+
* JSON.
|
|
19
|
+
* @throws 'body too large' past the cap, or the JSON.parse error for an
|
|
20
|
+
* invalid or empty payload.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readBoundedJson(req: IncomingMessage, maxBytes: number): Promise<unknown>;
|
|
23
|
+
/**
|
|
24
|
+
* Lenient bounded body reader: parse a request body as JSON, or null on an
|
|
25
|
+
* empty body, invalid JSON, or a body past maxBytes (default 64 KiB).
|
|
26
|
+
* Overflow destroys the request instead of draining the remainder (no drain
|
|
27
|
+
* call, matching the current repo-wide behavior); callers must not keep
|
|
28
|
+
* reading the request afterwards. With objectOnly, non-JSON-object payloads
|
|
29
|
+
* also yield null.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readJsonBody(req: IncomingMessage, opts?: {
|
|
32
|
+
maxBytes?: number;
|
|
33
|
+
objectOnly?: boolean;
|
|
34
|
+
}): Promise<unknown | null>;
|
|
35
|
+
/** Narrow a value to a JSON object, or undefined when it is not one. */
|
|
36
|
+
export declare function asJsonObject(value: unknown): Record<string, unknown> | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Write one JSON response. Default headers are the family defaults
|
|
39
|
+
* (content-type and referrer-policy); caller headers are appended or
|
|
40
|
+
* override them.
|
|
41
|
+
*/
|
|
42
|
+
export declare function writeJson(res: ServerResponse, status: number, body: unknown, headers?: OutgoingHttpHeaders): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-task-board",
|
|
3
3
|
"description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -52,9 +52,11 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
52
52
|
return (
|
|
53
53
|
<div className={css.board} data-dsh-taskboard-board="" data-dsh-plugin="task-board">
|
|
54
54
|
<header className={css.boardHeader}>
|
|
55
|
+
{/* Shared hook: dsh-web-ui-all offsets center-view back controls beside the collapsed mobile sidebar. */}
|
|
55
56
|
<button
|
|
56
57
|
type="button"
|
|
57
58
|
className={`${css.ghostButton} ${css.backButton}`}
|
|
59
|
+
data-dsh-center-view-back=""
|
|
58
60
|
aria-label={t('board.close')}
|
|
59
61
|
onClick={() => { controller.closeBoard() }}
|
|
60
62
|
>
|
|
@@ -51,12 +51,13 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
51
51
|
/* --- sidebar entry row ------------------------------------------------------- */
|
|
52
52
|
|
|
53
53
|
.entry {
|
|
54
|
+
box-sizing: border-box;
|
|
54
55
|
display: flex;
|
|
55
56
|
align-items: center;
|
|
56
57
|
gap: 8px;
|
|
57
58
|
width: 100%;
|
|
58
|
-
height:
|
|
59
|
-
padding: 0
|
|
59
|
+
height: 36px;
|
|
60
|
+
padding: 0 10px;
|
|
60
61
|
background: transparent;
|
|
61
62
|
border: none;
|
|
62
63
|
border-radius: 8px;
|
|
@@ -67,12 +68,12 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
.entry:hover {
|
|
70
|
-
background: var(--dsw-
|
|
71
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
71
72
|
color: var(--dsw-alias-label-primary);
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
.entry[data-active] {
|
|
75
|
-
background: var(--dsw-
|
|
76
|
+
background: var(--dsw-alias-interactive-bg-active);
|
|
76
77
|
color: var(--dsw-alias-label-primary);
|
|
77
78
|
font-weight: 600;
|
|
78
79
|
}
|
|
@@ -81,9 +82,17 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
81
82
|
display: inline-flex;
|
|
82
83
|
align-items: center;
|
|
83
84
|
justify-content: center;
|
|
85
|
+
width: 24px;
|
|
86
|
+
height: 24px;
|
|
84
87
|
flex: none;
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
.entryIcon svg {
|
|
91
|
+
display: block;
|
|
92
|
+
width: 18px;
|
|
93
|
+
height: 18px;
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
.entryLabel {
|
|
88
97
|
overflow: hidden;
|
|
89
98
|
text-overflow: ellipsis;
|
|
@@ -93,7 +102,10 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
93
102
|
[data-dsh-frame][data-sidebar-collapsed] .entry {
|
|
94
103
|
justify-content: center;
|
|
95
104
|
padding: 0;
|
|
96
|
-
width:
|
|
105
|
+
width: 36px;
|
|
106
|
+
height: 36px;
|
|
107
|
+
margin: 0 auto 12px;
|
|
108
|
+
border-radius: 50%;
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
[data-dsh-frame][data-sidebar-collapsed] .entryLabel {
|
|
@@ -809,31 +821,191 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
809
821
|
|
|
810
822
|
/* --- constrained board containers ------------------------------------------ */
|
|
811
823
|
|
|
812
|
-
@container task-board-view (max-width:
|
|
824
|
+
@container task-board-view (max-width: 768px) {
|
|
813
825
|
.board {
|
|
814
826
|
gap: 10px;
|
|
815
827
|
padding: 10px;
|
|
816
828
|
}
|
|
817
829
|
|
|
830
|
+
.boardHeader {
|
|
831
|
+
align-items: center;
|
|
832
|
+
flex-wrap: wrap;
|
|
833
|
+
gap: 8px;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.backButton {
|
|
837
|
+
flex: 0 0 auto;
|
|
838
|
+
order: 1;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.boardTitle {
|
|
842
|
+
flex: 1 1 auto;
|
|
843
|
+
order: 2;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.boardHeader > .detailMeta {
|
|
847
|
+
flex: 1 0 100%;
|
|
848
|
+
margin-left: 0;
|
|
849
|
+
order: 3;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.search {
|
|
853
|
+
flex: 1 0 100%;
|
|
854
|
+
min-width: 0;
|
|
855
|
+
order: 4;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.boardHeader > button:not(.backButton) {
|
|
859
|
+
flex: 1 1 0;
|
|
860
|
+
min-width: 0;
|
|
861
|
+
order: 5;
|
|
862
|
+
}
|
|
863
|
+
|
|
818
864
|
.columns {
|
|
865
|
+
grid-auto-columns: 86cqw;
|
|
819
866
|
gap: 10px;
|
|
867
|
+
padding-inline: 2px 14cqw;
|
|
868
|
+
scroll-padding-inline: 2px;
|
|
869
|
+
scroll-snap-type: inline mandatory;
|
|
870
|
+
scrollbar-width: none;
|
|
871
|
+
-webkit-overflow-scrolling: touch;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
.columns::-webkit-scrollbar {
|
|
875
|
+
display: none;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.column {
|
|
879
|
+
scroll-snap-align: start;
|
|
880
|
+
scroll-snap-stop: always;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
@container task-board-view (max-width: 720px) {
|
|
885
|
+
.boardHeader > .detailMeta {
|
|
886
|
+
overflow: hidden;
|
|
887
|
+
text-overflow: ellipsis;
|
|
888
|
+
white-space: nowrap;
|
|
820
889
|
}
|
|
821
890
|
}
|
|
822
891
|
|
|
823
892
|
@container task-board-view (max-width: 600px) {
|
|
824
|
-
.
|
|
893
|
+
.board {
|
|
894
|
+
padding-inline: 8px;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/* Viewport-specific mobile treatment: overlays occupy the dynamic viewport,
|
|
899
|
+
preserve notches/home indicators, and keep their action rows reachable. */
|
|
900
|
+
@media (max-width: 768px) {
|
|
901
|
+
[data-dsh-taskboard-view] {
|
|
902
|
+
height: 100vh;
|
|
903
|
+
height: 100dvh;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.entry,
|
|
907
|
+
.card,
|
|
908
|
+
.primaryButton,
|
|
909
|
+
.ghostButton,
|
|
910
|
+
.dangerButton,
|
|
911
|
+
.iconButton,
|
|
912
|
+
.linkButton,
|
|
913
|
+
.search,
|
|
914
|
+
.input,
|
|
915
|
+
.select,
|
|
916
|
+
.schedulePreset,
|
|
917
|
+
.scheduleToggle {
|
|
918
|
+
min-height: 44px;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.search,
|
|
922
|
+
.input,
|
|
923
|
+
.select,
|
|
924
|
+
.schedulePreset {
|
|
925
|
+
box-sizing: border-box;
|
|
926
|
+
font-size: 16px;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.modalBackdrop {
|
|
930
|
+
align-items: stretch;
|
|
931
|
+
justify-content: stretch;
|
|
932
|
+
width: 100vw;
|
|
933
|
+
height: 100vh;
|
|
934
|
+
height: 100dvh;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.modal,
|
|
938
|
+
.detail {
|
|
939
|
+
box-sizing: border-box;
|
|
940
|
+
width: 100vw;
|
|
941
|
+
height: 100vh;
|
|
942
|
+
height: 100dvh;
|
|
943
|
+
max-height: none;
|
|
944
|
+
border: 0;
|
|
945
|
+
border-radius: 0;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
.modal {
|
|
949
|
+
padding-top: max(16px, env(safe-area-inset-top));
|
|
950
|
+
padding-right: max(16px, env(safe-area-inset-right));
|
|
951
|
+
padding-bottom: max(16px, env(safe-area-inset-bottom));
|
|
952
|
+
padding-left: max(16px, env(safe-area-inset-left));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.modalFooter {
|
|
956
|
+
position: sticky;
|
|
957
|
+
bottom: 0;
|
|
958
|
+
z-index: 1;
|
|
825
959
|
flex-wrap: wrap;
|
|
826
|
-
|
|
960
|
+
padding-top: 8px;
|
|
961
|
+
background: var(--dsw-alias-bg-base);
|
|
827
962
|
}
|
|
828
963
|
|
|
829
|
-
.
|
|
830
|
-
flex: 1 1
|
|
964
|
+
.modalFooter > button {
|
|
965
|
+
flex: 1 1 120px;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
.detailHeader {
|
|
969
|
+
flex-wrap: wrap;
|
|
970
|
+
padding-top: max(12px, env(safe-area-inset-top));
|
|
971
|
+
padding-right: max(14px, env(safe-area-inset-right));
|
|
972
|
+
padding-left: max(14px, env(safe-area-inset-left));
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
.detailTitle {
|
|
831
976
|
min-width: 0;
|
|
832
977
|
}
|
|
833
978
|
|
|
834
|
-
.
|
|
835
|
-
|
|
836
|
-
|
|
979
|
+
.detailBody {
|
|
980
|
+
overscroll-behavior-y: contain;
|
|
981
|
+
padding-right: max(14px, env(safe-area-inset-right));
|
|
982
|
+
padding-left: max(14px, env(safe-area-inset-left));
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
.detailFooter {
|
|
986
|
+
flex-wrap: wrap;
|
|
987
|
+
padding-right: max(14px, env(safe-area-inset-right));
|
|
988
|
+
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
|
989
|
+
padding-left: max(14px, env(safe-area-inset-left));
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.detailFooter > button {
|
|
993
|
+
flex: 1 1 96px;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.detailFooter > .detailMeta {
|
|
997
|
+
flex: 1 0 100%;
|
|
998
|
+
margin-left: 0;
|
|
999
|
+
text-align: end;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.scheduleRow {
|
|
1003
|
+
align-items: stretch;
|
|
1004
|
+
flex-direction: column;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.schedulePreset {
|
|
1008
|
+
width: 100%;
|
|
837
1009
|
}
|
|
838
1010
|
}
|
|
839
1011
|
|
|
@@ -19,8 +19,8 @@ import { mountSidebarEntry as mountSharedSidebarEntry } from './sidebar-entry-co
|
|
|
19
19
|
/** Stable data attribute identifying the injected entry row. */
|
|
20
20
|
export const ENTRY_SELECTOR = '[data-dsh-taskboard-entry]'
|
|
21
21
|
|
|
22
|
-
/** Inline icon
|
|
23
|
-
const ICON = '<svg viewBox="0 0 16 16" width="
|
|
22
|
+
/** Inline icon normalized to the shell's 18px navigation glyph size. */
|
|
23
|
+
const ICON = '<svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M6.5 6.5v7"/></svg>'
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Mount the sidebar entry, waiting for the shell to render and self-healing
|