Compare commits
2 Commits
1716351016
...
4b372100eb
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b372100eb | |||
| 6cc51a35c4 |
@@ -1,24 +1,664 @@
|
||||
{
|
||||
"queue": [
|
||||
{
|
||||
"created_at": "2026-06-13T07:19:31Z",
|
||||
"finished_at": "2026-06-13T07:19:32Z",
|
||||
"id": "3a520bd342883c75",
|
||||
"created_at": "2026-06-15T03:25:12Z",
|
||||
"finished_at": "2026-06-15T03:26:42Z",
|
||||
"id": "6732b109c5f13b8f",
|
||||
"log": [
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Loop 1/1",
|
||||
"ts": "2026-06-13T07:19:31Z"
|
||||
"message": "Loop endless (simulated, max 10000)",
|
||||
"ts": "2026-06-15T03:25:12Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-13T07:19:31Z"
|
||||
"ts": "2026-06-15T03:25:12Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-13T07:19:31Z"
|
||||
"ts": "2026-06-15T03:25:13Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:14Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:14Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:15Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:16Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:17Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:17Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:18Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:18Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:19Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:20Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:21Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:21Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:22Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:23Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:24Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:24Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:25Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:25Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:26Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:27Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:28Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:28Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:29Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:30Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:31Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:31Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:32Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:32Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:33Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:34Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:35Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:35Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:36Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:37Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:38Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:38Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:39Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:39Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:40Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:41Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:42Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:42Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:43Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:44Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:45Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:45Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:46Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:46Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:47Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:48Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:49Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:49Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:50Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:51Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:52Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:52Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:53Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:53Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:54Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:55Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:56Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:56Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:57Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:58Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:25:59Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:25:59Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:00Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:00Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:01Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:02Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:03Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:03Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:04Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:05Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:06Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:06Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:07Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:07Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:08Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:09Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:10Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:10Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:11Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:12Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:13Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:13Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:14Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:14Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:15Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:16Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:17Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:17Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:18Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:19Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:20Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:20Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:21Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:21Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:22Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:23Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:24Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:24Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:25Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:26Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:27Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:27Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:28Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:28Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:29Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:30Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:31Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:31Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:32Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:33Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:34Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:34Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:35Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:35Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:36Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:37Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:38Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:38Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:39Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:40Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:41Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-15T03:26:41Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-15T03:26:42Z"
|
||||
},
|
||||
{
|
||||
"level": "warn",
|
||||
"message": "Mission hủy bởi operator",
|
||||
"ts": "2026-06-15T03:26:42Z"
|
||||
}
|
||||
],
|
||||
"mission": {
|
||||
@@ -50,8 +690,8 @@
|
||||
"kind": "action",
|
||||
"label": "Loop",
|
||||
"params": {
|
||||
"count": 1,
|
||||
"mode": "count"
|
||||
"count": 0,
|
||||
"mode": "endless"
|
||||
},
|
||||
"type": "loop"
|
||||
}
|
||||
@@ -60,7 +700,7 @@
|
||||
"group": "Missions",
|
||||
"id": "5ae9dbcb0722dffb",
|
||||
"name": "Test run",
|
||||
"updated_at": "2026-06-13T07:19:17.185Z"
|
||||
"updated_at": "2026-06-15T03:08:55.138Z"
|
||||
},
|
||||
"mission_group": "Missions",
|
||||
"mission_id": "5ae9dbcb0722dffb",
|
||||
@@ -69,88 +709,16 @@
|
||||
"priority": 0,
|
||||
"robot_id": "default",
|
||||
"source": "ui",
|
||||
"started_at": "2026-06-13T07:19:31Z",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"created_at": "2026-06-13T07:19:41Z",
|
||||
"finished_at": "2026-06-13T07:19:43Z",
|
||||
"id": "51de82d74cb4b1cd",
|
||||
"log": [
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Loop 1/1",
|
||||
"ts": "2026-06-13T07:19:41Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-13T07:19:41Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-13T07:19:42Z"
|
||||
}
|
||||
],
|
||||
"mission": {
|
||||
"actions": [
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"id": "c6c40563-0755-4e97-a48a-bb91ac8b0a9c",
|
||||
"kind": "action",
|
||||
"label": "Set PLC register",
|
||||
"params": {
|
||||
"action": "set",
|
||||
"register": 1,
|
||||
"value": 0
|
||||
},
|
||||
"type": "set_plc_register"
|
||||
},
|
||||
{
|
||||
"id": "a1",
|
||||
"kind": "action",
|
||||
"label": "Wait",
|
||||
"params": {
|
||||
"seconds": 1
|
||||
},
|
||||
"type": "wait"
|
||||
}
|
||||
],
|
||||
"id": "65f3cf0b-73fa-4f51-8774-1c5d4c83d8c4",
|
||||
"kind": "action",
|
||||
"label": "Loop",
|
||||
"params": {
|
||||
"count": 1,
|
||||
"mode": "count"
|
||||
},
|
||||
"type": "loop"
|
||||
}
|
||||
],
|
||||
"description": "",
|
||||
"group": "Missions",
|
||||
"id": "5ae9dbcb0722dffb",
|
||||
"name": "Test run",
|
||||
"updated_at": "2026-06-13T07:19:17.185Z"
|
||||
},
|
||||
"mission_group": "Missions",
|
||||
"mission_id": "5ae9dbcb0722dffb",
|
||||
"mission_name": "Test run",
|
||||
"parameters": {},
|
||||
"priority": 0,
|
||||
"robot_id": "default",
|
||||
"source": "ui",
|
||||
"started_at": "2026-06-13T07:19:41Z",
|
||||
"status": "completed"
|
||||
"started_at": "2026-06-15T03:25:12Z",
|
||||
"status": "cancelled"
|
||||
}
|
||||
],
|
||||
"runner": {
|
||||
"current_action": null,
|
||||
"current_queue_id": null,
|
||||
"message": "Hoàn thành: Test run",
|
||||
"message": "Đã hủy: Test run",
|
||||
"paused": false,
|
||||
"state": "idle",
|
||||
"updated_at": "2026-06-13T07:19:43Z"
|
||||
"updated_at": "2026-06-15T03:26:42Z"
|
||||
}
|
||||
}
|
||||
@@ -40,8 +40,8 @@
|
||||
"kind": "action",
|
||||
"label": "Loop",
|
||||
"params": {
|
||||
"count": 1,
|
||||
"mode": "count"
|
||||
"count": 0,
|
||||
"mode": "endless"
|
||||
},
|
||||
"type": "loop"
|
||||
}
|
||||
@@ -50,7 +50,7 @@
|
||||
"group": "Missions",
|
||||
"id": "5ae9dbcb0722dffb",
|
||||
"name": "Test run",
|
||||
"updated_at": "2026-06-13T07:19:17.185Z"
|
||||
"updated_at": "2026-06-15T03:08:55.138Z"
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
|
||||
@@ -126,10 +126,37 @@ if [[ "$RUNNER_STATE" == "running" ]]; then
|
||||
assert_code "POST /api/mission_queue/continue" 200 "$TMP/cont.json" \
|
||||
-X POST "$BASE/api/mission_queue/continue"
|
||||
assert_json_true "runner not paused" "$TMP/cont.json" 'doc.get("state") != "paused"'
|
||||
for _ in $(seq 1 15); do
|
||||
curl -s "$BASE/api/mission_queue" -o "$TMP/runner_poll.json"
|
||||
RUNNER_STATE="$(python3 -c "import json; print(json.load(open('$TMP/runner_poll.json')).get('runner',{}).get('state',''))")"
|
||||
if [[ "$RUNNER_STATE" == "running" || "$RUNNER_STATE" == "paused" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
if [[ "$RUNNER_STATE" == "running" || "$RUNNER_STATE" == "paused" ]]; then
|
||||
assert_code "POST /api/mission_queue/cancel" 200 "$TMP/cancel.json" \
|
||||
-X POST "$BASE/api/mission_queue/cancel"
|
||||
for _ in $(seq 1 40); do
|
||||
curl -s "$BASE/api/mission_queue" -o "$TMP/runner_poll.json"
|
||||
RUNNER_STATE="$(python3 -c "import json; print(json.load(open('$TMP/runner_poll.json')).get('runner',{}).get('state',''))")"
|
||||
if [[ "$RUNNER_STATE" == "idle" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.15
|
||||
done
|
||||
assert_json_true "runner idle after cancel" "$TMP/runner_poll.json" \
|
||||
'doc.get("runner",{}).get("state") == "idle"'
|
||||
else
|
||||
log_fail "runner not active for cancel test (skipped)"
|
||||
fi
|
||||
else
|
||||
log_fail "runner never reached running (pause test skipped)"
|
||||
fi
|
||||
|
||||
assert_code "POST /api/mission_queue/cancel idle" 400 "$TMP/cancel_idle.json" \
|
||||
-X POST "$BASE/api/mission_queue/cancel"
|
||||
|
||||
# --- LiDAR CRUD ---
|
||||
assert_code "POST /api/lidars" 201 "$TMP/lidar.json" \
|
||||
-X POST "$BASE/api/lidars" \
|
||||
|
||||
@@ -12,6 +12,12 @@ namespace lm {
|
||||
|
||||
namespace {
|
||||
|
||||
class MissionCancelled : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
MissionCancelled() : std::runtime_error("mission cancelled") {}
|
||||
};
|
||||
|
||||
std::string paramValue(const std::string& action_id,
|
||||
const nlohmann::json& params,
|
||||
const std::string& key,
|
||||
@@ -319,6 +325,30 @@ bool MissionQueue::resume(std::string& err)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MissionQueue::cancel(std::string& err)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mu_);
|
||||
const std::string state = runner_.value("state", "idle");
|
||||
if (state != "running" && state != "paused")
|
||||
{
|
||||
err = "no mission is running";
|
||||
return false;
|
||||
}
|
||||
if (cancel_)
|
||||
{
|
||||
err = "cancel already in progress";
|
||||
return false;
|
||||
}
|
||||
cancel_ = true;
|
||||
paused_ = false;
|
||||
runner_["paused"] = false;
|
||||
runner_["message"] = "Đang hủy mission…";
|
||||
runner_["updated_at"] = IdUtil::nowIso8601();
|
||||
saveUnlocked();
|
||||
wake_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void MissionQueue::workerLoop()
|
||||
{
|
||||
while (!stop_)
|
||||
@@ -382,14 +412,17 @@ void MissionQueue::workerLoop()
|
||||
|
||||
void MissionQueue::runMissionActions(nlohmann::json& entry)
|
||||
{
|
||||
cancel_ = false;
|
||||
nlohmann::json log = nlohmann::json::array();
|
||||
try
|
||||
{
|
||||
nlohmann::json log = nlohmann::json::array();
|
||||
const auto& mission = entry["mission"];
|
||||
const auto& parameters = entry["parameters"];
|
||||
const auto& actions =
|
||||
mission.contains("actions") && mission["actions"].is_array() ? mission["actions"] : nlohmann::json::array();
|
||||
executeActionsUnlocked(actions, parameters, log, 0);
|
||||
if (cancel_)
|
||||
throw MissionCancelled();
|
||||
entry["log"] = log;
|
||||
entry["status"] = "completed";
|
||||
entry["finished_at"] = IdUtil::nowIso8601();
|
||||
@@ -399,6 +432,18 @@ void MissionQueue::runMissionActions(nlohmann::json& entry)
|
||||
saveUnlocked();
|
||||
}
|
||||
}
|
||||
catch (const MissionCancelled&)
|
||||
{
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()}, {"level", "warn"}, {"message", "Mission hủy bởi operator"}});
|
||||
entry["log"] = log;
|
||||
entry["status"] = "cancelled";
|
||||
entry["finished_at"] = IdUtil::nowIso8601();
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mu_);
|
||||
setRunnerState("idle", "Đã hủy: " + entry.value("mission_name", "Mission"));
|
||||
saveUnlocked();
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
entry["status"] = "failed";
|
||||
@@ -409,6 +454,7 @@ void MissionQueue::runMissionActions(nlohmann::json& entry)
|
||||
saveUnlocked();
|
||||
}
|
||||
}
|
||||
cancel_ = false;
|
||||
}
|
||||
|
||||
MissionQueue::LoopControl MissionQueue::executeActionsUnlocked(const nlohmann::json& actions,
|
||||
@@ -423,10 +469,12 @@ MissionQueue::LoopControl MissionQueue::executeActionsUnlocked(const nlohmann::j
|
||||
{
|
||||
if (!action.is_object())
|
||||
continue;
|
||||
while (paused_ && !stop_)
|
||||
while (paused_ && !stop_ && !cancel_)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (stop_)
|
||||
return LoopControl::None;
|
||||
if (cancel_)
|
||||
throw MissionCancelled();
|
||||
|
||||
const std::string action_id = action.value("id", "");
|
||||
const std::string kind = action.value("kind", "action");
|
||||
@@ -478,7 +526,7 @@ MissionQueue::LoopControl MissionQueue::executeActionsUnlocked(const nlohmann::j
|
||||
const auto& children =
|
||||
action.contains("children") && action["children"].is_array() ? action["children"] : nlohmann::json::array();
|
||||
const int iterations = mode == "endless" ? 10000 : std::max(1, count);
|
||||
for (int i = 0; i < iterations && !stop_; ++i)
|
||||
for (int i = 0; i < iterations && !stop_ && !cancel_; ++i)
|
||||
{
|
||||
if (mode == "endless" && i == 0)
|
||||
{
|
||||
@@ -497,6 +545,8 @@ MissionQueue::LoopControl MissionQueue::executeActionsUnlocked(const nlohmann::j
|
||||
break;
|
||||
if (ctrl == LoopControl::Continue)
|
||||
continue;
|
||||
if (cancel_)
|
||||
throw MissionCancelled();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -506,6 +556,8 @@ MissionQueue::LoopControl MissionQueue::executeActionsUnlocked(const nlohmann::j
|
||||
const int ms = static_cast<int>(paramNumber(params, "seconds", 1) * 1000);
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()}, {"level", "info"}, {"message", "Wait " + std::to_string(ms) + "ms"}});
|
||||
sleepMs(ms);
|
||||
if (cancel_)
|
||||
throw MissionCancelled();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -516,6 +568,8 @@ MissionQueue::LoopControl MissionQueue::executeActionsUnlocked(const nlohmann::j
|
||||
{"level", "info"},
|
||||
{"message", label + " → " + (pos.empty() ? "?" : pos)}});
|
||||
sleepMs(1200);
|
||||
if (cancel_)
|
||||
throw MissionCancelled();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -526,6 +580,8 @@ MissionQueue::LoopControl MissionQueue::executeActionsUnlocked(const nlohmann::j
|
||||
{"level", "info"},
|
||||
{"message", label + " → " + (marker.empty() ? "?" : marker)}});
|
||||
sleepMs(1200);
|
||||
if (cancel_)
|
||||
throw MissionCancelled();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -534,6 +590,8 @@ MissionQueue::LoopControl MissionQueue::executeActionsUnlocked(const nlohmann::j
|
||||
const std::string message = params.value("message", "Mission step");
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()}, {"level", "user"}, {"message", message}});
|
||||
sleepMs(200);
|
||||
if (cancel_)
|
||||
throw MissionCancelled();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -541,12 +599,16 @@ MissionQueue::LoopControl MissionQueue::executeActionsUnlocked(const nlohmann::j
|
||||
{
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()}, {"level", "warn"}, {"message", "Pause (simulated)"}});
|
||||
sleepMs(500);
|
||||
if (cancel_)
|
||||
throw MissionCancelled();
|
||||
continue;
|
||||
}
|
||||
|
||||
log.push_back(
|
||||
{{"ts", IdUtil::nowIso8601()}, {"level", "info"}, {"message", label + " (" + type + ") simulated"}});
|
||||
sleepMs(400);
|
||||
if (cancel_)
|
||||
throw MissionCancelled();
|
||||
}
|
||||
return LoopControl::None;
|
||||
}
|
||||
@@ -556,9 +618,9 @@ void MissionQueue::sleepMs(int ms)
|
||||
if (ms <= 0)
|
||||
return;
|
||||
const int step = 100;
|
||||
for (int elapsed = 0; elapsed < ms && !stop_; elapsed += step)
|
||||
for (int elapsed = 0; elapsed < ms && !stop_ && !cancel_; elapsed += step)
|
||||
{
|
||||
while (paused_ && !stop_)
|
||||
while (paused_ && !stop_ && !cancel_)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(std::min(step, ms - elapsed)));
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
bool reorder(const nlohmann::json& ordered_ids, std::string& err);
|
||||
bool pause(std::string& err);
|
||||
bool resume(std::string& err);
|
||||
bool cancel(std::string& err);
|
||||
|
||||
private:
|
||||
enum class LoopControl { None, Break, Continue };
|
||||
@@ -42,6 +43,7 @@ private:
|
||||
std::atomic<bool> stop_{false};
|
||||
std::atomic<bool> wake_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<bool> cancel_{false};
|
||||
|
||||
void load();
|
||||
void saveUnlocked() const;
|
||||
|
||||
@@ -526,6 +526,15 @@ void ApiServer::registerRoutes(httplib::Server& svr)
|
||||
res.body = mission_queue_.runnerStatus().dump();
|
||||
});
|
||||
|
||||
svr.Post("/api/mission_queue/cancel", [this](const httplib::Request&, httplib::Response& res) {
|
||||
HttpUtil::addCors(res);
|
||||
std::string err;
|
||||
if (!mission_queue_.cancel(err))
|
||||
return HttpUtil::jsonError(res, 400, err);
|
||||
res.set_header("Content-Type", "application/json; charset=utf-8");
|
||||
res.body = mission_queue_.runnerStatus().dump();
|
||||
});
|
||||
|
||||
registerMissionRoutes(svr);
|
||||
registerIntegrationRoutes(svr);
|
||||
registerMirV2Routes(svr);
|
||||
|
||||
Binary file not shown.
@@ -49,6 +49,29 @@ def mission_id(api):
|
||||
return resolve_mission_id(api)
|
||||
|
||||
|
||||
def clear_queue(api: requests.Session) -> None:
|
||||
api.delete(f"{BASE}/api/mission_queue", timeout=TIMEOUT)
|
||||
|
||||
|
||||
def wait_runner_state(api: requests.Session, state: str, timeout: float = 8.0) -> dict:
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
runner = api.get(f"{BASE}/api/mission_queue", timeout=TIMEOUT).json().get("runner", {})
|
||||
if runner.get("state") == state:
|
||||
return runner
|
||||
time.sleep(0.15)
|
||||
pytest.fail(f"runner did not reach state {state!r} within {timeout}s")
|
||||
|
||||
|
||||
def enqueue_mission(api: requests.Session, mission_id: str) -> None:
|
||||
r = api.post(
|
||||
f"{BASE}/api/mission_queue",
|
||||
json={"mission_id": mission_id},
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
assert r.status_code == 201
|
||||
|
||||
|
||||
def test_health(api):
|
||||
r = api.get(f"{BASE}/api/health", timeout=TIMEOUT)
|
||||
assert r.status_code == 200
|
||||
@@ -80,20 +103,9 @@ def test_mir_v2_enqueue_and_list(api, mission_id):
|
||||
|
||||
|
||||
def test_queue_pause_continue(api, mission_id):
|
||||
api.delete(f"{BASE}/api/mission_queue", timeout=TIMEOUT)
|
||||
api.post(
|
||||
f"{BASE}/api/mission_queue",
|
||||
json={"mission_id": mission_id},
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
deadline = time.time() + 5
|
||||
while time.time() < deadline:
|
||||
runner = api.get(f"{BASE}/api/mission_queue", timeout=TIMEOUT).json().get("runner", {})
|
||||
if runner.get("state") == "running":
|
||||
break
|
||||
time.sleep(0.2)
|
||||
else:
|
||||
pytest.skip("runner did not enter running state in time")
|
||||
clear_queue(api)
|
||||
enqueue_mission(api, mission_id)
|
||||
wait_runner_state(api, "running", timeout=5)
|
||||
|
||||
r = api.post(f"{BASE}/api/mission_queue/pause", timeout=TIMEOUT)
|
||||
assert r.status_code == 200
|
||||
@@ -104,6 +116,48 @@ def test_queue_pause_continue(api, mission_id):
|
||||
assert r.json().get("state") != "paused"
|
||||
|
||||
|
||||
def test_queue_cancel_rejects_when_idle(api):
|
||||
clear_queue(api)
|
||||
r = api.post(f"{BASE}/api/mission_queue/cancel", timeout=TIMEOUT)
|
||||
assert r.status_code == 400
|
||||
body = r.json()
|
||||
assert "error" in body
|
||||
|
||||
|
||||
def test_queue_cancel_stops_running_mission(api, mission_id):
|
||||
clear_queue(api)
|
||||
enqueue_mission(api, mission_id)
|
||||
wait_runner_state(api, "running", timeout=5)
|
||||
|
||||
r = api.post(f"{BASE}/api/mission_queue/cancel", timeout=TIMEOUT)
|
||||
assert r.status_code == 200
|
||||
assert r.json().get("message")
|
||||
|
||||
wait_runner_state(api, "idle", timeout=8)
|
||||
data = api.get(f"{BASE}/api/mission_queue", timeout=TIMEOUT).json()
|
||||
assert data["runner"]["state"] == "idle"
|
||||
|
||||
cancelled = [item for item in data.get("queue", []) if item.get("status") == "cancelled"]
|
||||
assert cancelled, "expected a cancelled queue entry"
|
||||
assert cancelled[0].get("mission_id") == mission_id
|
||||
log = cancelled[0].get("log") or []
|
||||
assert any(entry.get("message") == "Mission hủy bởi operator" for entry in log if isinstance(entry, dict))
|
||||
|
||||
|
||||
def test_queue_cancel_rejects_while_cancelling(api, mission_id):
|
||||
clear_queue(api)
|
||||
enqueue_mission(api, mission_id)
|
||||
wait_runner_state(api, "running", timeout=5)
|
||||
|
||||
first = api.post(f"{BASE}/api/mission_queue/cancel", timeout=TIMEOUT)
|
||||
assert first.status_code == 200
|
||||
|
||||
second = api.post(f"{BASE}/api/mission_queue/cancel", timeout=TIMEOUT)
|
||||
assert second.status_code == 400
|
||||
|
||||
wait_runner_state(api, "idle", timeout=8)
|
||||
|
||||
|
||||
def test_modbus_trigger_flow(api, mission_id):
|
||||
trig = api.post(
|
||||
f"{BASE}/api/triggers",
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
<label>Tiêu đề widget (tùy chọn)</label>
|
||||
<input data-field="title" type="text" value="${escapeHtml(widget.title || "")}" />
|
||||
</div>
|
||||
<p class="mutedNote">Tạm dừng / tiếp tục mission đang chạy trên robot.</p>`;
|
||||
<p class="mutedNote">Tạm dừng / tiếp tục / hủy mission đang chạy trên robot.</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,9 +206,14 @@
|
||||
const paused = state === "paused" || snap?.runner?.paused;
|
||||
const running = state === "running" || paused;
|
||||
bodyEl.innerHTML = `
|
||||
<button type="button" class="dashboardPauseBtn ${paused ? "is-paused" : ""}" data-pause-action="${paused ? "continue" : "pause"}" ${running ? "" : "disabled"}>
|
||||
${paused ? "Continue" : "Pause"}
|
||||
</button>
|
||||
<div class="dashboardRunnerControls">
|
||||
<button type="button" class="dashboardPauseBtn ${paused ? "is-paused" : ""}" data-pause-action="${paused ? "continue" : "pause"}" ${running ? "" : "disabled"}>
|
||||
${paused ? "Continue" : "Pause"}
|
||||
</button>
|
||||
<button type="button" class="dashboardCancelBtn" data-cancel-mission ${running ? "" : "disabled"}>
|
||||
Hủy mission
|
||||
</button>
|
||||
</div>
|
||||
<p class="mutedNote dashboardWidgetHint">${running ? (paused ? "Mission đang tạm dừng" : "Mission đang chạy") : "Không có mission đang chạy"}</p>`;
|
||||
bodyEl.querySelector("[data-pause-action]")?.addEventListener("click", async (evt) => {
|
||||
const action = evt.currentTarget.dataset.pauseAction;
|
||||
@@ -219,6 +224,13 @@
|
||||
alert(e.message);
|
||||
}
|
||||
});
|
||||
bodyEl.querySelector("[data-cancel-mission]")?.addEventListener("click", async () => {
|
||||
try {
|
||||
await missions()?.cancelRunner?.();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderWidget(widget) {
|
||||
|
||||
@@ -547,7 +547,10 @@
|
||||
<div class="cardTitle">Mission queue</div>
|
||||
<div class="cardSub">Thêm mission bằng biểu tượng queue — robot chạy theo thứ tự từ trên xuống.</div>
|
||||
</div>
|
||||
<button id="missionQueueClearBtn" type="button" class="btn subtle danger">Xóa queue</button>
|
||||
<div class="missionQueueCardActions">
|
||||
<button id="missionQueueCancelBtn" type="button" class="btn subtle danger" title="Hủy mission đang chạy">Hủy chạy</button>
|
||||
<button id="missionQueueClearBtn" type="button" class="btn subtle danger">Xóa queue</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cardBody">
|
||||
<div id="missionQueueRunner" class="missionQueueRunner mutedNote">—</div>
|
||||
|
||||
@@ -473,6 +473,7 @@
|
||||
executing: "Đang chạy",
|
||||
completed: "Xong",
|
||||
failed: "Lỗi",
|
||||
cancelled: "Đã hủy",
|
||||
};
|
||||
return map[status] || status;
|
||||
}
|
||||
@@ -639,6 +640,12 @@
|
||||
await refreshQueue();
|
||||
}
|
||||
|
||||
async function cancelRunner() {
|
||||
if (!confirm("Hủy mission đang chạy? (thoát loop và dừng ngay)")) return;
|
||||
await missionApi("/api/mission_queue/cancel", { method: "POST", body: "{}" });
|
||||
await refreshQueue();
|
||||
}
|
||||
|
||||
function openQueueDialog(missionId) {
|
||||
const mission = findMission(missionId);
|
||||
if (!mission) return;
|
||||
@@ -850,15 +857,19 @@
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="missionDragHandle" draggable="true" title="Kéo để sắp xếp" aria-label="Kéo để sắp xếp">↕</div>
|
||||
<div class="missionActionMain">
|
||||
<div class="missionActionLabelRow">
|
||||
<span class="missionActionIcon ${iconClass}">${iconChar}</span>
|
||||
<span class="missionActionLabel">${escapeHtml(action.label)}</span>
|
||||
<div class="missionActionTop">
|
||||
<div class="missionActionMain">
|
||||
<div class="missionActionLabelRow">
|
||||
<span class="missionActionIcon ${iconClass}">${iconChar}</span>
|
||||
<span class="missionActionLabel">${escapeHtml(action.label)}</span>
|
||||
</div>
|
||||
<div class="missionActionSummary">${escapeHtml(actionSummary(action))}</div>
|
||||
</div>
|
||||
<div class="missionActionSummary">${escapeHtml(actionSummary(action))}</div>
|
||||
</div>
|
||||
<button type="button" class="iconBtn" data-config="${action.id}" title="Cấu hình">⚙</button>
|
||||
<button type="button" class="iconBtn danger" data-remove="${action.id}" title="Xóa">×</button>`;
|
||||
<div class="missionActionBtns">
|
||||
<button type="button" class="iconBtn" data-config="${action.id}" title="Cấu hình">⚙</button>
|
||||
<button type="button" class="iconBtn danger" data-remove="${action.id}" title="Xóa">×</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (action.type === "loop" && Array.isArray(action.children)) {
|
||||
const loop = document.createElement("div");
|
||||
@@ -876,11 +887,15 @@
|
||||
renderActionRows(action.children, `${listPath}.${action.id}`, drop);
|
||||
}
|
||||
loop.appendChild(drop);
|
||||
row.querySelector(".missionActionMain").appendChild(loop);
|
||||
row.appendChild(loop);
|
||||
}
|
||||
|
||||
row.querySelector("[data-config]").addEventListener("click", () => openActionConfig(action.id));
|
||||
row.querySelector("[data-remove]").addEventListener("click", () => {
|
||||
row.querySelector("[data-config]").addEventListener("click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
openActionConfig(action.id);
|
||||
});
|
||||
row.querySelector("[data-remove]").addEventListener("click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
removeActionFromTree(action.id);
|
||||
renderMissionEditor();
|
||||
});
|
||||
@@ -1308,6 +1323,9 @@
|
||||
});
|
||||
|
||||
el("missionQueueClearBtn")?.addEventListener("click", clearQueue);
|
||||
el("missionQueueCancelBtn")?.addEventListener("click", () => {
|
||||
cancelRunner().catch((e) => alert(e.message));
|
||||
});
|
||||
el("missionQueueForm")?.addEventListener("submit", submitQueueDialog);
|
||||
}
|
||||
|
||||
@@ -1327,6 +1345,7 @@
|
||||
enqueueMission,
|
||||
pauseRunner,
|
||||
continueRunner,
|
||||
cancelRunner,
|
||||
refreshQueue,
|
||||
clearQueue,
|
||||
getQueueSnapshot,
|
||||
|
||||
@@ -622,6 +622,9 @@ canvas {
|
||||
.missionQueueStatus.executing { background: #dbeafe; color: #1d4ed8; }
|
||||
.missionQueueStatus.completed { background: #d1fae5; color: #047857; }
|
||||
.missionQueueStatus.failed { background: #fee2e2; color: #b91c1c; }
|
||||
.missionQueueStatus.cancelled { background: #f3f4f6; color: #6b7280; }
|
||||
.missionQueueItem.status-cancelled { opacity: 0.72; }
|
||||
.missionQueueCardActions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.missionQueueRunner {
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
@@ -730,7 +733,7 @@ canvas {
|
||||
|
||||
.missionActionRow {
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr auto auto;
|
||||
grid-template-columns: 32px 1fr;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
padding: 10px 12px;
|
||||
@@ -739,6 +742,28 @@ canvas {
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
.missionActionTop {
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.missionActionBtns {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.missionLoopBlock {
|
||||
grid-column: 2;
|
||||
margin-top: 0;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed rgba(124, 58, 237, 0.35);
|
||||
background: rgba(124, 58, 237, 0.04);
|
||||
padding: 10px;
|
||||
}
|
||||
.missionActionRow.dragging { opacity: 0.45; }
|
||||
.missionActionRow.dropBefore { box-shadow: inset 0 3px 0 var(--accent); }
|
||||
.missionActionRow.dropAfter { box-shadow: inset 0 -3px 0 var(--accent); }
|
||||
@@ -789,13 +814,6 @@ canvas {
|
||||
.iconBtn:hover { border-color: rgba(37, 99, 235, 0.35); color: var(--accent); background: #eff6ff; }
|
||||
.iconBtn.danger:hover { border-color: rgba(239, 68, 68, 0.35); color: var(--danger); background: #fef2f2; }
|
||||
|
||||
.missionLoopBlock {
|
||||
margin-top: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed rgba(124, 58, 237, 0.35);
|
||||
background: rgba(124, 58, 237, 0.04);
|
||||
padding: 10px;
|
||||
}
|
||||
.missionLoopLabel {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
@@ -950,6 +968,21 @@ canvas {
|
||||
}
|
||||
.dashboardPauseBtn.is-paused { background: #ecfdf5; color: #047857; }
|
||||
.dashboardPauseBtn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.dashboardRunnerControls { display: grid; gap: 8px; }
|
||||
.dashboardCancelBtn {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(239, 68, 68, 0.35);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
.dashboardCancelBtn:hover:not(:disabled) { background: #fee2e2; }
|
||||
.dashboardCancelBtn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.dashboardInfoCard .dashboardInfoGrid { display: grid; gap: 8px; }
|
||||
.dashboardEmpty { text-align: center; padding: 12px 0 0; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user