@sleep2agi/agent-network-dashboard 0.6.3-preview.49 → 0.6.3-preview.50
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/.next/BUILD_COMMIT +1 -1
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +97 -97
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +5 -5
- package/.next/server/app/_not-found.rsc +13 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +5 -5
- package/.next/server/app/admin.rsc +15 -15
- package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
- package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
- package/.next/server/app/admin.segments/_index.segment.rsc +8 -8
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
- package/.next/server/app/index.html +5 -5
- package/.next/server/app/index.rsc +15 -15
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +8 -8
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +2 -2
- package/.next/server/app/login.rsc +15 -15
- package/.next/server/app/login.segments/_full.segment.rsc +15 -15
- package/.next/server/app/login.segments/_head.segment.rsc +4 -4
- package/.next/server/app/login.segments/_index.segment.rsc +8 -8
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/login.segments/login.segment.rsc +3 -3
- package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +5 -5
- package/.next/server/app/logs.rsc +15 -15
- package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/logs.segments/_index.segment.rsc +8 -8
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
- package/.next/server/app/messages/page.js.nft.json +1 -1
- package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +5 -5
- package/.next/server/app/messages.rsc +15 -15
- package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
- package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
- package/.next/server/app/messages.segments/_index.segment.rsc +8 -8
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
- package/.next/server/app/node/page.js.nft.json +1 -1
- package/.next/server/app/node/page_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +5 -5
- package/.next/server/app/node.rsc +15 -15
- package/.next/server/app/node.segments/_full.segment.rsc +15 -15
- package/.next/server/app/node.segments/_head.segment.rsc +4 -4
- package/.next/server/app/node.segments/_index.segment.rsc +8 -8
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/node.segments/node.segment.rsc +3 -3
- package/.next/server/app/nodes/[alias]/page.js.nft.json +1 -1
- package/.next/server/app/nodes/[alias]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes/page.js.nft.json +1 -1
- package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes.html +5 -5
- package/.next/server/app/nodes.rsc +15 -15
- package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
- package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/_index.segment.rsc +8 -8
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers.html +5 -5
- package/.next/server/app/providers.rsc +15 -15
- package/.next/server/app/providers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/providers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/providers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/providers.segments/providers.segment.rsc +3 -3
- package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +5 -5
- package/.next/server/app/server-logs.rsc +15 -15
- package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/_index.segment.rsc +8 -8
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
- package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/servers.html +5 -5
- package/.next/server/app/servers.rsc +15 -15
- package/.next/server/app/servers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/servers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/servers.segments/servers.segment.rsc +3 -3
- package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +5 -5
- package/.next/server/app/settings/networks.rsc +15 -15
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +5 -5
- package/.next/server/app/settings/tokens.rsc +15 -15
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings.html +5 -5
- package/.next/server/app/settings.rsc +15 -15
- package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page.js.nft.json +1 -1
- package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks.html +5 -5
- package/.next/server/app/tasks.rsc +15 -15
- package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/_index.segment.rsc +8 -8
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__05z_qq6._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__05z_qq6._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js.map +1 -0
- package/.next/server/chunks/ssr/{dash-stageb-wt_app_0e7vp44._.js → dash-stageb-wt_app_045dcq5._.js} +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_045dcq5._.js.map +1 -0
- package/.next/server/chunks/ssr/{dash-stageb-wt_app_0g~a.7i._.js → dash-stageb-wt_app_05u4_h~._.js} +8 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.js.map +1 -0
- package/.next/server/chunks/ssr/{dash-stageb-wt_app_0qpn85e._.js → dash-stageb-wt_app_0nuizh~._.js} +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0nuizh~._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_components_0vzsmx1._.js +5 -5
- package/.next/server/chunks/ssr/dash-stageb-wt_app_components_0vzsmx1._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js +3 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js.map +1 -0
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +5 -5
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/02.5_nifak.uu.js +1 -0
- package/.next/static/chunks/03hzj129k~odb.js +1 -0
- package/.next/static/chunks/06q7-vhy~1-y3.js +7 -0
- package/.next/static/chunks/0_4ljrbp.stxy.js +1 -0
- package/.next/static/chunks/0cey4i.y0a8~j.js +1 -0
- package/.next/static/chunks/{0i8qbq4v47v3r.js → 0ep78sq90x_yc.js} +1 -1
- package/.next/static/chunks/{0_xyyy_x0k42j.js → 0hm52b0y~k0~z.js} +1 -1
- package/.next/static/chunks/0kncmrm2a_q43.js +1 -0
- package/.next/static/chunks/0mhaf6lrw5nn..css +1 -0
- package/.next/static/chunks/{12jbb28xy2hw..js → 0qf8xjihw3zi..js} +1 -1
- package/.next/static/chunks/0xyu92zdvvghl.js +7 -0
- package/.next/static/chunks/0zj_navpyhltx.js +2 -0
- package/.next/static/chunks/10px7ehmo7eq0.js +1 -0
- package/.next/static/chunks/{0tv-.l-_mw4ye.js → 11zz15q37~bz5.js} +1 -1
- package/.next/static/chunks/14y4x3de1lbug.js +1 -0
- package/.next/static/chunks/15ka68xi.w-i-.js +1 -0
- package/.next/static/chunks/17qkc714xy04..js +1 -0
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/ChatPane.tsx +2 -2
- package/app/components/NodeList.tsx +40 -4
- package/app/components/Sidebar.tsx +180 -29
- package/app/components/TaskChatPanel.tsx +1 -1
- package/app/components/TaskDetail.tsx +407 -0
- package/app/components/TaskList.tsx +117 -0
- package/app/nodes/NodesView.tsx +28 -3
- package/app/tasks/TasksView.tsx +353 -0
- package/app/tasks/[id]/page.tsx +23 -176
- package/app/tasks/page.tsx +10 -453
- package/package.json +1 -1
- package/tests/e2e-sidebar-default.spec.ts +141 -0
- package/tests/e2e-sidebar-icon-rail.spec.ts +261 -0
- package/tests/e2e-stage-b-chat-pane.spec.ts +286 -0
- package/tests/e2e-stage-tasks-two-column.spec.ts +353 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0b-b6yh._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0b-b6yh._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0fs-j~c._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0fs-j~c._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0ma5-5u._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0ma5-5u._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0~hwpo-._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0~hwpo-._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_095q0~q._.js +0 -3
- package/.next/server/chunks/ssr/dash-stageb-wt_app_095q0~q._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0e7vp44._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0g~a.7i._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qpn85e._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0tppjr_._.js +0 -9
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0tppjr_._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0y764mt._.js +0 -3
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0y764mt._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_page_tsx_0.fhtup._.js +0 -3
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_page_tsx_0.fhtup._.js.map +0 -1
- package/.next/static/chunks/027yjbjm9afuk.js +0 -1
- package/.next/static/chunks/07wf61~wof.l~.js +0 -1
- package/.next/static/chunks/0ev252k-tf_aa.css +0 -1
- package/.next/static/chunks/0j-jjs3w89dei.js +0 -1
- package/.next/static/chunks/0ko39v-gezo7d.js +0 -1
- package/.next/static/chunks/0l8vf4q2t5x4d.js +0 -7
- package/.next/static/chunks/0xcazs6t8~l4q.js +0 -2
- package/.next/static/chunks/10sr651~7vm42.js +0 -7
- package/.next/static/chunks/13_i5b0qmko7z.js +0 -1
- package/.next/static/chunks/140suz13o9va6.js +0 -1
- package/.next/static/chunks/144cf7ds3_9~b.js +0 -1
- package/.next/static/chunks/17c4wlsbmmgk9.js +0 -1
- package/.next/static/chunks/17cfly-04z1j0.js +0 -1
- /package/.next/static/{HIKVEkFShWE481BxQDjwH → vc_aJHtYwefsVcl2_f14Z}/_buildManifest.js +0 -0
- /package/.next/static/{HIKVEkFShWE481BxQDjwH → vc_aJHtYwefsVcl2_f14Z}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{HIKVEkFShWE481BxQDjwH → vc_aJHtYwefsVcl2_f14Z}/_ssgManifest.js +0 -0
package/app/tasks/page.tsx
CHANGED
|
@@ -1,459 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { TASK_STATUSES, STATUS_CHIP_CLASS, STATUS_DOT_HEX, STATUS_BAR_CLASS } from '../lib/status';
|
|
3
|
+
// #Stage-tasks: /tasks entry point. Thin wrapper — the whole layout
|
|
4
|
+
// lives in TasksView so /tasks and /tasks/[id] share the same code
|
|
5
|
+
// path (only diff: [id] page passes an initialSelectedId that seeds
|
|
6
|
+
// the deep-link selection).
|
|
7
|
+
//
|
|
8
|
+
// The 21.5KB one-big-table + inline-expand + TaskDrawer implementation
|
|
9
|
+
// that used to live here is replaced by the list+detail layout — see
|
|
10
|
+
// TasksView.tsx header comment for the design rationale.
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
task_id: string;
|
|
15
|
-
network_id?: string;
|
|
16
|
-
from_name: string;
|
|
17
|
-
to_name: string;
|
|
18
|
-
status: string;
|
|
19
|
-
priority: string;
|
|
20
|
-
content: string;
|
|
21
|
-
result: string;
|
|
22
|
-
created_at: string;
|
|
23
|
-
updated_at: string;
|
|
24
|
-
delivered_at: string;
|
|
25
|
-
started_at: string;
|
|
26
|
-
completed_at: string;
|
|
27
|
-
expires_at: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function statusBadge(status: string) {
|
|
31
|
-
const color = STATUS_CHIP_CLASS[status] || 'bg-gray-500/10 text-gray-400 border-gray-500/20';
|
|
32
|
-
return `text-xs px-2 py-0.5 rounded-md border ${color}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function priorityBadge(priority: string) {
|
|
36
|
-
if (priority === 'high') return 'text-red-400';
|
|
37
|
-
if (priority === 'low') return 'text-gray-600';
|
|
38
|
-
return 'text-gray-400';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function timeAgo(dateStr: string): string {
|
|
42
|
-
if (!dateStr) return '--';
|
|
43
|
-
const diff = Date.now() - new Date(dateStr.replace(' ', 'T') + 'Z').getTime();
|
|
44
|
-
const s = Math.floor(diff / 1000);
|
|
45
|
-
if (s < 60) return `${s}s ago`;
|
|
46
|
-
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
47
|
-
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
|
|
48
|
-
return `${Math.floor(s / 86400)}d ago`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function TasksContent() {
|
|
52
|
-
const searchParams = useSearchParams();
|
|
53
|
-
const { networkId } = useNetworkId();
|
|
54
|
-
const [tasks, setTasks] = useState<Task[]>([]);
|
|
55
|
-
const [count, setCount] = useState(0);
|
|
56
|
-
const [loading, setLoading] = useState(true);
|
|
57
|
-
const [error, setError] = useState('');
|
|
58
|
-
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
59
|
-
|
|
60
|
-
const [filterStatus, setFilterStatus] = useState(searchParams.get('status') || '');
|
|
61
|
-
const [filterFrom, setFilterFrom] = useState('');
|
|
62
|
-
const [filterTo, setFilterTo] = useState('');
|
|
63
|
-
const [drawerTaskId, setDrawerTaskId] = useState<string | null>(null);
|
|
64
|
-
|
|
65
|
-
const toggleExpand = (taskId: string) => {
|
|
66
|
-
setExpanded(prev => {
|
|
67
|
-
const next = new Set(prev);
|
|
68
|
-
if (next.has(taskId)) next.delete(taskId);
|
|
69
|
-
else next.add(taskId);
|
|
70
|
-
return next;
|
|
71
|
-
});
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const fetchTasks = useCallback(async () => {
|
|
75
|
-
try {
|
|
76
|
-
const params = new URLSearchParams();
|
|
77
|
-
if (networkId) params.set('network_id', networkId);
|
|
78
|
-
if (filterStatus) params.set('status', filterStatus);
|
|
79
|
-
if (filterFrom) params.set('from_name', filterFrom);
|
|
80
|
-
if (filterTo) params.set('to_name', filterTo);
|
|
81
|
-
params.set('limit', '100');
|
|
82
|
-
|
|
83
|
-
const res = await fetch(`/api/hub/tasks?${params.toString()}`);
|
|
84
|
-
if (res.status === 401) {
|
|
85
|
-
window.location.assign('/login');
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const data = await res.json();
|
|
89
|
-
setTasks(data.tasks || []);
|
|
90
|
-
setCount(data.count ?? (data.tasks?.length || 0));
|
|
91
|
-
setError('');
|
|
92
|
-
} catch (e: unknown) {
|
|
93
|
-
setError(e instanceof Error ? e.message : 'fetch failed');
|
|
94
|
-
} finally {
|
|
95
|
-
setLoading(false);
|
|
96
|
-
}
|
|
97
|
-
}, [filterStatus, filterFrom, filterTo, networkId]);
|
|
98
|
-
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
setLoading(true);
|
|
101
|
-
fetchTasks();
|
|
102
|
-
const interval = setInterval(fetchTasks, 5000);
|
|
103
|
-
return () => clearInterval(interval);
|
|
104
|
-
}, [fetchTasks]);
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<div className="min-h-screen bg-[#0b0b0d] text-gray-100 p-4 sm:p-6">
|
|
108
|
-
{/* Header.
|
|
109
|
-
#209 R30 (mobile vertical rhythm — goal "大幅提升移动端体验",
|
|
110
|
-
extending R28's Overview pattern to /tasks): this mb-6 + the
|
|
111
|
-
From/To filter row mb-6 below both drop to mb-4 on phones,
|
|
112
|
-
restoring mb-6 from sm: up. Saves ~16 px per spot = ~32 px of
|
|
113
|
-
scroll reclaim on /tasks before the actual task rows. /tasks
|
|
114
|
-
is the second-most-trafficked page after Overview. */}
|
|
115
|
-
<div className="flex items-center gap-4 mb-4 sm:mb-6">
|
|
116
|
-
<h1 className="text-2xl font-bold text-white lg:ml-0 ml-10">Tasks</h1>
|
|
117
|
-
{/* Round 88 made this chip pagination-aware; #217 D6 hides it
|
|
118
|
-
entirely in the common case — the All tab right below shows
|
|
119
|
-
the same count. It only renders when it carries unique info
|
|
120
|
-
(loaded < total ⇒ the list is a slice). */}
|
|
121
|
-
{tasks.length < count && (
|
|
122
|
-
<span
|
|
123
|
-
className="text-xs bg-blue-900/30 text-blue-400 px-2 py-0.5 rounded-full border border-blue-800/30 tabular-nums"
|
|
124
|
-
title={`Showing ${tasks.length} of ${count} tasks`}
|
|
125
|
-
>
|
|
126
|
-
{tasks.length} / {count}
|
|
127
|
-
</span>
|
|
128
|
-
)}
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
{/* Round 75: hide the status tab strip + From/To filter row when
|
|
132
|
-
there are no tasks at all. Each tab would show "0" and the
|
|
133
|
-
filter inputs would have nothing to act on — pure noise above
|
|
134
|
-
the empty-state CTA. When at least one task exists, the chrome
|
|
135
|
-
comes back even if the current filter happens to hide
|
|
136
|
-
everything (so users can clear filters). */}
|
|
137
|
-
{tasks.length > 0 && <>
|
|
138
|
-
{/* Status Tabs — color-coded dots per status family so users can
|
|
139
|
-
scan the row at a glance. Active tab gets full chip styling
|
|
140
|
-
(bg + border + text color), inactive tabs only carry the dot
|
|
141
|
-
+ neutral label so the active state remains the strong cue.
|
|
142
|
-
On narrow viewports (<sm) the row scrolls horizontally with
|
|
143
|
-
subtle gradient fade-edges (via .anet-tabstrip-wrap pseudo
|
|
144
|
-
elements) hinting more content. */}
|
|
145
|
-
<div className="anet-tabstrip-wrap mb-4">
|
|
146
|
-
<div className="anet-tabstrip flex sm:flex-wrap gap-1 bg-[#161618] rounded-lg border border-[#26262b] p-1 overflow-x-auto sm:overflow-x-visible scrollbar-thin">
|
|
147
|
-
{(() => {
|
|
148
|
-
// Round 56: compute per-status counts so the tab strip doubles as a
|
|
149
|
-
// status distribution dashboard. Matches Audit Log (r43) + Overview
|
|
150
|
-
// agent filter (r34) chip-with-count pattern.
|
|
151
|
-
const counts: Record<string, number> = {};
|
|
152
|
-
tasks.forEach(t => { counts[t.status] = (counts[t.status] || 0) + 1; });
|
|
153
|
-
return ['', ...TASK_STATUSES].map(s => {
|
|
154
|
-
const count = s === '' ? tasks.length : counts[s] || 0;
|
|
155
|
-
const isActive = filterStatus === s;
|
|
156
|
-
return (
|
|
157
|
-
<button
|
|
158
|
-
key={s}
|
|
159
|
-
onClick={() => setFilterStatus(s)}
|
|
160
|
-
disabled={count === 0 && s !== '' && !isActive}
|
|
161
|
-
className={`px-3 py-1.5 rounded-md text-xs transition-colors flex items-center gap-1.5 shrink-0 whitespace-nowrap disabled:opacity-30 disabled:cursor-not-allowed ${
|
|
162
|
-
isActive
|
|
163
|
-
? `${STATUS_CHIP_CLASS[s] || 'bg-cyan-500/10 text-cyan-300 border-cyan-500/20'} border`
|
|
164
|
-
: 'text-gray-500 hover:text-gray-300 hover:bg-[#1c1c1f]/40'
|
|
165
|
-
}`}
|
|
166
|
-
>
|
|
167
|
-
{s && (
|
|
168
|
-
<span
|
|
169
|
-
aria-hidden
|
|
170
|
-
className="inline-block w-1.5 h-1.5 rounded-full shrink-0"
|
|
171
|
-
style={{ backgroundColor: STATUS_DOT_HEX[s] || '#6b7280' }}
|
|
172
|
-
/>
|
|
173
|
-
)}
|
|
174
|
-
<span>{s || 'All'}</span>
|
|
175
|
-
<span className={`text-[10px] tabular-nums ${isActive ? 'opacity-80' : 'text-gray-600'}`}>{count}</span>
|
|
176
|
-
</button>
|
|
177
|
-
);
|
|
178
|
-
});
|
|
179
|
-
})()}
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
182
|
-
|
|
183
|
-
{/* From/To Filters — same visual block as the status tabs above.
|
|
184
|
-
R30 (see header above): mb-6 → mb-4 sm:mb-6 mobile-tighten. */}
|
|
185
|
-
<div className="flex flex-wrap items-center gap-2 mb-4 sm:mb-6">
|
|
186
|
-
<div className="flex items-center gap-1.5 rounded-lg border border-[#26262b] bg-[#161618] px-2.5 py-1.5 focus-within:border-blue-500/40">
|
|
187
|
-
<span className="text-[10px] uppercase tracking-wide text-gray-600">From</span>
|
|
188
|
-
<input
|
|
189
|
-
type="text"
|
|
190
|
-
value={filterFrom}
|
|
191
|
-
onChange={e => setFilterFrom(e.target.value)}
|
|
192
|
-
placeholder="any node"
|
|
193
|
-
className="w-28 bg-transparent text-base sm:text-sm text-white placeholder-gray-700 focus:outline-none"
|
|
194
|
-
/>
|
|
195
|
-
</div>
|
|
196
|
-
<div className="flex items-center gap-1.5 rounded-lg border border-[#26262b] bg-[#161618] px-2.5 py-1.5 focus-within:border-blue-500/40">
|
|
197
|
-
<span className="text-[10px] uppercase tracking-wide text-gray-600">To</span>
|
|
198
|
-
<input
|
|
199
|
-
type="text"
|
|
200
|
-
value={filterTo}
|
|
201
|
-
onChange={e => setFilterTo(e.target.value)}
|
|
202
|
-
placeholder="any node"
|
|
203
|
-
className="w-28 bg-transparent text-base sm:text-sm text-white placeholder-gray-700 focus:outline-none"
|
|
204
|
-
/>
|
|
205
|
-
</div>
|
|
206
|
-
{(filterStatus || filterFrom || filterTo) && (
|
|
207
|
-
<button
|
|
208
|
-
type="button"
|
|
209
|
-
onClick={() => { setFilterStatus(''); setFilterFrom(''); setFilterTo(''); }}
|
|
210
|
-
className="rounded-lg border border-gray-700 px-2.5 py-1.5 text-[11px] text-gray-500 hover:text-gray-200 hover:border-gray-600"
|
|
211
|
-
>
|
|
212
|
-
Clear filters
|
|
213
|
-
</button>
|
|
214
|
-
)}
|
|
215
|
-
</div>
|
|
216
|
-
</>}
|
|
217
|
-
|
|
218
|
-
{/* Status distribution */}
|
|
219
|
-
{tasks.length > 0 && !filterStatus && (() => {
|
|
220
|
-
const stats: Record<string, number> = {};
|
|
221
|
-
tasks.forEach(t => { stats[t.status] = (stats[t.status] || 0) + 1; });
|
|
222
|
-
const total = tasks.length || 1;
|
|
223
|
-
// Round 67: derive from shared TASK_STATUSES + STATUS_BAR_CLASS in
|
|
224
|
-
// app/lib/status.ts. Adding a new lifecycle status updates badge,
|
|
225
|
-
// tab dots, and bar in one edit.
|
|
226
|
-
const bars = TASK_STATUSES
|
|
227
|
-
.map(key => ({ key, color: STATUS_BAR_CLASS[key] }))
|
|
228
|
-
.filter(b => stats[b.key]);
|
|
229
|
-
if (!bars.length) return null;
|
|
230
|
-
// Round 63: removed the per-status legend row beneath the bar —
|
|
231
|
-
// it duplicated the counts already shown in the tab chip strip
|
|
232
|
-
// since r56. The proportional bar stays because it adds a visual
|
|
233
|
-
// "shape of workload" that the chip numbers don't convey.
|
|
234
|
-
return (
|
|
235
|
-
<div className="mb-6">
|
|
236
|
-
<div className="flex h-2 rounded-full overflow-hidden bg-gray-800">
|
|
237
|
-
{bars.map(b => <div key={b.key} className={b.color} style={{ width: `${(stats[b.key]/total)*100}%` }} title={`${b.key}: ${stats[b.key]}`} />)}
|
|
238
|
-
</div>
|
|
239
|
-
</div>
|
|
240
|
-
);
|
|
241
|
-
})()}
|
|
242
|
-
|
|
243
|
-
{error && (
|
|
244
|
-
<div className="bg-red-900/20 border border-red-800/40 text-red-300 px-4 py-2 rounded-lg mb-6 text-sm">{error}</div>
|
|
245
|
-
)}
|
|
246
|
-
|
|
247
|
-
{loading ? (
|
|
248
|
-
<div className="animate-pulse space-y-3">
|
|
249
|
-
{[1, 2, 3, 4, 5].map(i => (
|
|
250
|
-
<div key={i} className="h-20 bg-gray-800/20 rounded-lg border border-gray-800/40" />
|
|
251
|
-
))}
|
|
252
|
-
</div>
|
|
253
|
-
) : tasks.length === 0 ? (
|
|
254
|
-
<EmptyState
|
|
255
|
-
variant="tasks"
|
|
256
|
-
sub={(filterStatus || filterFrom || filterTo)
|
|
257
|
-
? 'No tasks match the current filters. Try clearing them.'
|
|
258
|
-
: 'Tasks will appear here when agents send them via CommHub.'}
|
|
259
|
-
/>
|
|
260
|
-
) : (
|
|
261
|
-
<div className="space-y-1 sm:space-y-2">
|
|
262
|
-
{/* Table header */}
|
|
263
|
-
<div className="hidden sm:grid sm:grid-cols-12 gap-2 px-4 py-2 text-xs text-gray-600 uppercase">
|
|
264
|
-
<div className="col-span-1">Status</div>
|
|
265
|
-
<div className="col-span-2">From</div>
|
|
266
|
-
<div className="col-span-2">To</div>
|
|
267
|
-
<div className="col-span-4">Content</div>
|
|
268
|
-
<div className="col-span-1">Priority</div>
|
|
269
|
-
<div className="col-span-2">Time</div>
|
|
270
|
-
</div>
|
|
271
|
-
|
|
272
|
-
{tasks.map(t => {
|
|
273
|
-
const isOpen = expanded.has(t.task_id);
|
|
274
|
-
return (
|
|
275
|
-
<div
|
|
276
|
-
key={t.task_id}
|
|
277
|
-
className={`anet-task-row group bg-[#161618] border rounded-lg px-3 py-2 sm:px-4 sm:py-3 transition-all duration-200 cursor-pointer ${
|
|
278
|
-
isOpen
|
|
279
|
-
? 'border-[#3a3a41] shadow-lg shadow-black/20'
|
|
280
|
-
: 'border-[#26262b] hover:border-[#3a3a41] hover:bg-[#15152e]'
|
|
281
|
-
}`}
|
|
282
|
-
onClick={() => toggleExpand(t.task_id)}
|
|
283
|
-
role="button"
|
|
284
|
-
tabIndex={0}
|
|
285
|
-
aria-expanded={isOpen}
|
|
286
|
-
onKeyDown={(e) => {
|
|
287
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
288
|
-
e.preventDefault();
|
|
289
|
-
toggleExpand(t.task_id);
|
|
290
|
-
}
|
|
291
|
-
}}
|
|
292
|
-
>
|
|
293
|
-
{/* Desktop row */}
|
|
294
|
-
<div className="hidden sm:grid sm:grid-cols-12 gap-2 items-center">
|
|
295
|
-
<div className="col-span-1">
|
|
296
|
-
<span className={statusBadge(t.status)}>{t.status}</span>
|
|
297
|
-
</div>
|
|
298
|
-
<div className="col-span-2 flex items-center gap-1.5 min-w-0" title={t.from_name}>
|
|
299
|
-
{t.from_name && <AliasAvatar alias={t.from_name} size={18} />}
|
|
300
|
-
<span className="truncate text-sm text-gray-200">{t.from_name || '--'}</span>
|
|
301
|
-
</div>
|
|
302
|
-
<div className="col-span-2 flex items-center gap-1.5 min-w-0" title={t.to_name}>
|
|
303
|
-
{t.to_name && <AliasAvatar alias={t.to_name} size={18} />}
|
|
304
|
-
<span className="truncate text-sm text-gray-200">{t.to_name || '--'}</span>
|
|
305
|
-
</div>
|
|
306
|
-
<div className="col-span-4 text-xs text-gray-400 truncate" title={t.content}>
|
|
307
|
-
{previewContent(t.content)}
|
|
308
|
-
</div>
|
|
309
|
-
<div className="col-span-1">
|
|
310
|
-
<span className={`text-xs ${priorityBadge(t.priority)}`}>{t.priority || 'normal'}</span>
|
|
311
|
-
</div>
|
|
312
|
-
<div className="col-span-2 text-xs text-gray-500 flex items-center justify-between gap-2" title={t.created_at}>
|
|
313
|
-
<span className="truncate">{timeAgo(t.created_at)}</span>
|
|
314
|
-
<svg
|
|
315
|
-
aria-hidden
|
|
316
|
-
className={`shrink-0 text-gray-600 group-hover:text-gray-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
|
317
|
-
width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
|
|
318
|
-
>
|
|
319
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M6 9l6 6 6-6" />
|
|
320
|
-
</svg>
|
|
321
|
-
</div>
|
|
322
|
-
</div>
|
|
323
|
-
|
|
324
|
-
{/* Mobile layout — R8 of #190 mobile polish: 4-row stack
|
|
325
|
-
→ 3-row by inlining timeAgo onto the same row as the
|
|
326
|
-
from→to alias header (it's already 4 small atoms, has
|
|
327
|
-
room), plus space-y-2 → space-y-1 to trim ~4px per
|
|
328
|
-
row × 200 tasks. */}
|
|
329
|
-
<div className="sm:hidden space-y-1">
|
|
330
|
-
<div className="flex items-center justify-between">
|
|
331
|
-
<span className={statusBadge(t.status)}>{t.status}</span>
|
|
332
|
-
<div className="flex items-center gap-2">
|
|
333
|
-
<span className={`text-xs ${priorityBadge(t.priority)}`}>{t.priority || 'normal'}</span>
|
|
334
|
-
<svg
|
|
335
|
-
aria-hidden
|
|
336
|
-
className={`text-gray-600 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
|
337
|
-
width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
|
|
338
|
-
>
|
|
339
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M6 9l6 6 6-6" />
|
|
340
|
-
</svg>
|
|
341
|
-
</div>
|
|
342
|
-
</div>
|
|
343
|
-
<div className="flex items-center gap-1.5 text-xs text-gray-300 min-w-0">
|
|
344
|
-
{t.from_name && <AliasAvatar alias={t.from_name} size={16} />}
|
|
345
|
-
<span className="truncate max-w-[35%]">{t.from_name || '--'}</span>
|
|
346
|
-
<span className="text-gray-600">→</span>
|
|
347
|
-
{t.to_name && <AliasAvatar alias={t.to_name} size={16} />}
|
|
348
|
-
<span className="truncate max-w-[35%]">{t.to_name || '--'}</span>
|
|
349
|
-
<span className="ml-auto shrink-0 text-[10px] text-gray-600">{timeAgo(t.created_at)}</span>
|
|
350
|
-
</div>
|
|
351
|
-
<div className="text-xs text-gray-400 line-clamp-1" title={t.content}>{previewContent(t.content)}</div>
|
|
352
|
-
</div>
|
|
353
|
-
|
|
354
|
-
{/* Expanded detail — always mounted; grid-rows 0fr↔1fr trick gives
|
|
355
|
-
content-aware smooth max-height animation without measuring DOM. */}
|
|
356
|
-
<div
|
|
357
|
-
className={`grid transition-all duration-300 ease-out ${
|
|
358
|
-
isOpen ? 'grid-rows-[1fr] opacity-100 mt-3' : 'grid-rows-[0fr] opacity-0'
|
|
359
|
-
}`}
|
|
360
|
-
aria-hidden={!isOpen}
|
|
361
|
-
>
|
|
362
|
-
<div className="overflow-hidden">
|
|
363
|
-
<div className="pt-3 border-t border-[#26262b] space-y-3">
|
|
364
|
-
{t.content && (
|
|
365
|
-
<div>
|
|
366
|
-
<div className="text-xs text-gray-600 mb-1">Content</div>
|
|
367
|
-
<div className="text-xs text-gray-300 whitespace-pre-wrap bg-[#0e0e10] rounded-lg px-3 py-2 border border-[#1c1c1f]">{t.content}</div>
|
|
368
|
-
</div>
|
|
369
|
-
)}
|
|
370
|
-
{t.result && (
|
|
371
|
-
<div>
|
|
372
|
-
<div className="text-xs text-gray-600 mb-1">Result</div>
|
|
373
|
-
<div className="text-xs text-gray-300 whitespace-pre-wrap bg-[#0e0e10] rounded-lg px-3 py-2 border border-[#1c1c1f] max-h-48 overflow-y-auto">{t.result}</div>
|
|
374
|
-
</div>
|
|
375
|
-
)}
|
|
376
|
-
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-xs">
|
|
377
|
-
{[
|
|
378
|
-
['Created', t.created_at],
|
|
379
|
-
['Delivered', t.delivered_at],
|
|
380
|
-
['Started', t.started_at],
|
|
381
|
-
['Completed', t.completed_at],
|
|
382
|
-
].map(([label, val]) => (
|
|
383
|
-
<div key={label as string}>
|
|
384
|
-
<span className="text-gray-600">{label}: </span>
|
|
385
|
-
<span className="text-gray-400">{val ? timeAgo(val as string) : '--'}</span>
|
|
386
|
-
</div>
|
|
387
|
-
))}
|
|
388
|
-
</div>
|
|
389
|
-
{t.expires_at && (
|
|
390
|
-
<div className="text-xs">
|
|
391
|
-
<span className="text-gray-600">Expires: </span>
|
|
392
|
-
<span className="text-orange-400">{t.expires_at}</span>
|
|
393
|
-
</div>
|
|
394
|
-
)}
|
|
395
|
-
<div className="flex items-center justify-between gap-2">
|
|
396
|
-
<div className="text-xs text-gray-600 truncate" title={t.task_id}>
|
|
397
|
-
ID: {t.task_id}
|
|
398
|
-
</div>
|
|
399
|
-
<div className="flex gap-2 shrink-0">
|
|
400
|
-
{(t.status === 'failed' || t.status === 'expired') && (
|
|
401
|
-
<button
|
|
402
|
-
onClick={async (e) => {
|
|
403
|
-
e.stopPropagation();
|
|
404
|
-
await fetch('/api/hub/send', {
|
|
405
|
-
method: 'POST',
|
|
406
|
-
headers: { 'Content-Type': 'application/json' },
|
|
407
|
-
body: JSON.stringify({
|
|
408
|
-
alias: t.to_name, task: t.content, priority: t.priority,
|
|
409
|
-
// P0 network_id fix: retry in the task's own network.
|
|
410
|
-
...((t.network_id || networkId) ? { network_id: t.network_id || networkId } : {}),
|
|
411
|
-
}),
|
|
412
|
-
});
|
|
413
|
-
fetchTasks();
|
|
414
|
-
}}
|
|
415
|
-
className="text-xs text-orange-400 hover:text-orange-300 px-2 py-0.5 rounded border border-orange-500/20 hover:bg-orange-500/10"
|
|
416
|
-
>
|
|
417
|
-
Retry
|
|
418
|
-
</button>
|
|
419
|
-
)}
|
|
420
|
-
<button
|
|
421
|
-
onClick={e => { e.stopPropagation(); setDrawerTaskId(t.task_id); }}
|
|
422
|
-
className="text-xs text-cyan-400 hover:text-cyan-300"
|
|
423
|
-
>
|
|
424
|
-
Detail →
|
|
425
|
-
</button>
|
|
426
|
-
</div>
|
|
427
|
-
</div>
|
|
428
|
-
</div>
|
|
429
|
-
</div>
|
|
430
|
-
</div>
|
|
431
|
-
</div>
|
|
432
|
-
);
|
|
433
|
-
})}
|
|
434
|
-
</div>
|
|
435
|
-
)}
|
|
436
|
-
|
|
437
|
-
{drawerTaskId && <TaskDrawer taskId={drawerTaskId} onClose={() => setDrawerTaskId(null)} />}
|
|
438
|
-
</div>
|
|
439
|
-
);
|
|
440
|
-
}
|
|
12
|
+
import { TasksView } from './TasksView';
|
|
441
13
|
|
|
442
14
|
export default function TasksPage() {
|
|
443
|
-
return
|
|
444
|
-
<Suspense fallback={
|
|
445
|
-
// #209 R30 (playwright mobile-shot caught it): the bare Suspense
|
|
446
|
-
// fallback rendered "Loading tasks..." at p-6 with no left-indent,
|
|
447
|
-
// so the fixed top-3 left-3 mobile burger (56 px right-edge) sat
|
|
448
|
-
// right on top of "Loa" — the screenshot read as a broken page.
|
|
449
|
-
// Match the loaded layout: p-4 sm:p-6 page padding + ml-10 mobile
|
|
450
|
-
// indent on the text, so the burger clears and the loading state
|
|
451
|
-
// visually maps to the populated page.
|
|
452
|
-
<div className="min-h-screen bg-[#0b0b0d] text-gray-100 p-4 sm:p-6">
|
|
453
|
-
<div className="lg:ml-0 ml-10 text-gray-500 text-sm">Loading tasks…</div>
|
|
454
|
-
</div>
|
|
455
|
-
}>
|
|
456
|
-
<TasksContent />
|
|
457
|
-
</Suspense>
|
|
458
|
-
);
|
|
15
|
+
return <TasksView />;
|
|
459
16
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { test, expect, Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const BASE = process.env.TEST_URL || 'http://localhost:3100';
|
|
4
|
+
|
|
5
|
+
// 07-31 fix — Sidebar default should be 'icon-rail' for NEW visitors,
|
|
6
|
+
// but MUST NOT overwrite an existing choice in localStorage. The
|
|
7
|
+
// two are trivially entangled: setting the initial `useState` to
|
|
8
|
+
// 'icon-rail' fixes the first (default = narrow) and the useEffect
|
|
9
|
+
// that reads `anet-sidebar-mode` preserves the second (existing
|
|
10
|
+
// user's expanded/collapsed stays).
|
|
11
|
+
//
|
|
12
|
+
// 通信龙 07-31 review:
|
|
13
|
+
// > 只写第一条是不够的 — 第二条正是"改默认"最容易顺手破坏的东西,
|
|
14
|
+
// > 而且破坏了不会报错,用户只会觉得"这破玩意儿又变回去了"
|
|
15
|
+
//
|
|
16
|
+
// So we assert BOTH sides. Witnessed-red plan documented at the end
|
|
17
|
+
// of this file — mutation "remove the useEffect body" leaves test A
|
|
18
|
+
// green but reds test B, which is exactly the coverage claim.
|
|
19
|
+
|
|
20
|
+
// Per-worker login cache — the dashboard rate-limits logins to
|
|
21
|
+
// 10 / 15min / IP; per e2e-sidebar-icon-rail.spec.ts. Same pattern.
|
|
22
|
+
let cachedCookie: string | undefined;
|
|
23
|
+
|
|
24
|
+
async function login(context: Page['context'] extends () => infer C ? C : never) {
|
|
25
|
+
if (cachedCookie) {
|
|
26
|
+
await context.addCookies([{
|
|
27
|
+
name: 'anet_dashboard_session',
|
|
28
|
+
value: cachedCookie,
|
|
29
|
+
domain: new URL(BASE).hostname,
|
|
30
|
+
path: '/',
|
|
31
|
+
}]);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const res = await context.request.post(`${BASE}/api/auth/login`, {
|
|
35
|
+
data: { password: process.env.ANET_DASHBOARD_PASSWORD || 'admin123' },
|
|
36
|
+
});
|
|
37
|
+
const setCookie = res.headers()['set-cookie'] || '';
|
|
38
|
+
const m = setCookie.match(/anet_dashboard_session=([^;]+)/);
|
|
39
|
+
if (m) {
|
|
40
|
+
cachedCookie = decodeURIComponent(m[1]);
|
|
41
|
+
await context.addCookies([{
|
|
42
|
+
name: 'anet_dashboard_session',
|
|
43
|
+
value: cachedCookie,
|
|
44
|
+
domain: new URL(BASE).hostname,
|
|
45
|
+
path: '/',
|
|
46
|
+
}]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function stubHub(page: Page) {
|
|
51
|
+
await page.route('**/api/hub/status**', r =>
|
|
52
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ sessions: [] }) }));
|
|
53
|
+
await page.route('**/api/hub/nodes**', r =>
|
|
54
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ nodes: [] }) }));
|
|
55
|
+
await page.route('**/api/hub/networks**', r =>
|
|
56
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ networks: [{ network_id: 'net_x', network_name: 'net_x' }] }) }));
|
|
57
|
+
await page.route('**/api/hub/health', r =>
|
|
58
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, version: '0.9.0-test', sse_connections: 0, sse_sessions: {} }) }));
|
|
59
|
+
await page.route('**/api/hub/tasks**', r =>
|
|
60
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tasks: [] }) }));
|
|
61
|
+
await page.route('**/api/hub/stats**', r =>
|
|
62
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }));
|
|
63
|
+
await page.route('**/api/hub/messages**', r =>
|
|
64
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [] }) }));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test.describe('Sidebar default (07-31)', () => {
|
|
68
|
+
test('A — first-visit user (no localStorage) gets icon-rail by default, width ~56px', async ({ page, context }) => {
|
|
69
|
+
await login(context);
|
|
70
|
+
await stubHub(page);
|
|
71
|
+
// Make sure NOTHING is in localStorage — first-visit simulation.
|
|
72
|
+
await page.addInitScript(() => window.localStorage.removeItem('anet-sidebar-mode'));
|
|
73
|
+
await page.goto(`${BASE}/`);
|
|
74
|
+
await expect(page.locator('[data-anet-sidebar="true"]')).toHaveAttribute('data-sidebar-mode', 'icon-rail', { timeout: 15000 });
|
|
75
|
+
// Numeric width — spec's 56px target. Sub-pixel wiggle allowed
|
|
76
|
+
// (rounding / scrollbar). N站马's measurement of 207px on the
|
|
77
|
+
// pre-fix build corresponded to expanded (w-52 = 208px), so any
|
|
78
|
+
// number in this range is unambiguously the narrow form.
|
|
79
|
+
const w = await page.locator('[data-anet-sidebar="true"]').first().evaluate(el => el.getBoundingClientRect().width);
|
|
80
|
+
expect(w).toBeGreaterThanOrEqual(48);
|
|
81
|
+
expect(w).toBeLessThanOrEqual(64);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('B — returning user with expanded already stored is NOT overwritten', async ({ page, context }) => {
|
|
85
|
+
await login(context);
|
|
86
|
+
await stubHub(page);
|
|
87
|
+
// Seed the "existing user chose expanded" scenario — this is
|
|
88
|
+
// exactly the case a naive "just change the default" would break.
|
|
89
|
+
await page.addInitScript(() => window.localStorage.setItem('anet-sidebar-mode', 'expanded'));
|
|
90
|
+
await page.goto(`${BASE}/`);
|
|
91
|
+
// The rendered mode MUST honor stored choice, not overwrite it.
|
|
92
|
+
await expect(page.locator('[data-anet-sidebar="true"]')).toHaveAttribute('data-sidebar-mode', 'expanded', { timeout: 15000 });
|
|
93
|
+
// Width should match expanded (w-52 = ~208px), NOT icon-rail (56px).
|
|
94
|
+
const w = await page.locator('[data-anet-sidebar="true"]').first().evaluate(el => el.getBoundingClientRect().width);
|
|
95
|
+
expect(w).toBeGreaterThan(180);
|
|
96
|
+
// Sibling positive assertion — labels visible (proves it's actually
|
|
97
|
+
// expanded, not "sidebar failed to render past hydration"). Without
|
|
98
|
+
// this, a bug that fails to render the whole aside would still
|
|
99
|
+
// satisfy "not icon-rail width".
|
|
100
|
+
await expect(page.locator('[data-nav-item][data-nav-href="/"]')).toContainText('Overview');
|
|
101
|
+
// localStorage still holds 'expanded' after mount — not silently
|
|
102
|
+
// rewritten to something else.
|
|
103
|
+
expect(await page.evaluate(() => window.localStorage.getItem('anet-sidebar-mode'))).toBe('expanded');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('C — returning user with collapsed already stored is NOT overwritten either', async ({ page, context }) => {
|
|
107
|
+
await login(context);
|
|
108
|
+
await stubHub(page);
|
|
109
|
+
await page.addInitScript(() => window.localStorage.setItem('anet-sidebar-mode', 'collapsed'));
|
|
110
|
+
await page.goto(`${BASE}/`);
|
|
111
|
+
await expect(page.locator('[data-anet-sidebar="true"]')).toHaveAttribute('data-sidebar-mode', 'collapsed', { timeout: 15000 });
|
|
112
|
+
const w = await page.locator('[data-anet-sidebar="true"]').first().evaluate(el => el.getBoundingClientRect().width);
|
|
113
|
+
// w-16 = 64px
|
|
114
|
+
expect(w).toBeGreaterThanOrEqual(56);
|
|
115
|
+
expect(w).toBeLessThanOrEqual(80);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Witnessed-red plan (executed manually, documented here for future
|
|
120
|
+
// readers per 通信龙's "witnessed-red should show numbers" rule):
|
|
121
|
+
//
|
|
122
|
+
// Mutation: comment out the useEffect body in Sidebar.tsx that reads
|
|
123
|
+
// localStorage — the store-preservation path.
|
|
124
|
+
//
|
|
125
|
+
// useEffect(() => {
|
|
126
|
+
// // try {
|
|
127
|
+
// // const v = localStorage.getItem(SIDEBAR_MODE_STORAGE);
|
|
128
|
+
// // if (v && (SIDEBAR_MODES as readonly string[]).includes(v)) setMode(v as SidebarMode);
|
|
129
|
+
// // } catch {}
|
|
130
|
+
// }, []);
|
|
131
|
+
//
|
|
132
|
+
// Expected under mutation:
|
|
133
|
+
// - Test A stays GREEN. No LS means nothing to read anyway; default
|
|
134
|
+
// 'icon-rail' still applies.
|
|
135
|
+
// - Test B REDS. Expected data-sidebar-mode='expanded', received
|
|
136
|
+
// 'icon-rail'. Width also reds (Expected > 180, Received ~56).
|
|
137
|
+
// - Test C REDS. Same shape.
|
|
138
|
+
//
|
|
139
|
+
// If Test B stays green while A does too, the useEffect isn't actually
|
|
140
|
+
// standing between us and the "silently overwriting" failure — the
|
|
141
|
+
// two tests are testing the same thing and one of them is redundant.
|