|
1 | 1 | import { useState, useEffect, useCallback, useRef } from "react"; |
| 2 | +import { |
| 3 | + formatDateTimeLocalInput, |
| 4 | + formatTaskDateTime, |
| 5 | + formatTaskTime, |
| 6 | + parseTaskDateTime, |
| 7 | + serializeDateTimeLocalInput, |
| 8 | +} from "./dateTime.mjs"; |
2 | 9 |
|
3 | 10 | const API = "http://127.0.0.1:9712/api"; |
4 | 11 |
|
@@ -611,7 +618,7 @@ function TaskCard({ task, onAction, onViewDetail }) { |
611 | 618 | <Tag>⏳ {task.delay_seconds}s</Tag> |
612 | 619 | )} |
613 | 620 | {task.schedule_type === "scheduled_at" && task.next_run_at && ( |
614 | | - <Tag>📅 {new Date(task.next_run_at).toLocaleString()}</Tag> |
| 621 | + <Tag>📅 {formatTaskDateTime(task.next_run_at)}</Tag> |
615 | 622 | )} |
616 | 623 | {task.schedule_type === "cron" && ( |
617 | 624 | <Tag>⏲ {task.cron_expr}</Tag> |
@@ -639,7 +646,7 @@ function TaskCard({ task, onAction, onViewDetail }) { |
639 | 646 | {task.run_count > 0 && ( |
640 | 647 | <div style={{ fontSize: 10, color: theme.textDim, marginTop: 8, fontFamily: "monospace" }}> |
641 | 648 | Runs: {task.run_count}{task.max_runs ? ` / ${task.max_runs}` : ""} |
642 | | - {task.last_run_at && ` · Last: ${new Date(task.last_run_at).toLocaleTimeString()}`} |
| 649 | + {task.last_run_at && ` · Last: ${formatTaskTime(task.last_run_at)}`} |
643 | 650 | </div> |
644 | 651 | )} |
645 | 652 |
|
@@ -948,9 +955,9 @@ function HeartbeatCard({ heartbeat, onAction, onViewDetail }) { |
948 | 955 | </div> |
949 | 956 |
|
950 | 957 | <div style={{ fontSize: 11, color: theme.textDim, marginTop: 10, fontFamily: "monospace", lineHeight: 1.6 }}> |
951 | | - Next: {heartbeat.next_run_at ? new Date(heartbeat.next_run_at).toLocaleString() : "n/a"} |
| 958 | + Next: {heartbeat.next_run_at ? formatTaskDateTime(heartbeat.next_run_at) : "n/a"} |
952 | 959 | {" · "} |
953 | | - Triggered: {heartbeat.last_triggered_at ? new Date(heartbeat.last_triggered_at).toLocaleString() : "never"} |
| 960 | + Triggered: {heartbeat.last_triggered_at ? formatTaskDateTime(heartbeat.last_triggered_at) : "never"} |
954 | 961 | </div> |
955 | 962 | {heartbeat.last_error && ( |
956 | 963 | <div style={{ fontSize: 11, color: theme.red, marginTop: 6, lineHeight: 1.4 }}> |
@@ -1037,9 +1044,9 @@ function HeartbeatDetailPanel({ heartbeat, ticks, onClose }) { |
1037 | 1044 | {heartbeat.last_decision && <Tag>{heartbeat.last_decision}</Tag>} |
1038 | 1045 | </div> |
1039 | 1046 | <div style={{ fontSize: 12, color: theme.textMuted, lineHeight: 1.7, marginBottom: 18 }}> |
1040 | | - <div>Next run: {heartbeat.next_run_at ? new Date(heartbeat.next_run_at).toLocaleString() : "n/a"}</div> |
1041 | | - <div>Last tick: {heartbeat.last_tick_at ? new Date(heartbeat.last_tick_at).toLocaleString() : "never"}</div> |
1042 | | - <div>Last trigger: {heartbeat.last_triggered_at ? new Date(heartbeat.last_triggered_at).toLocaleString() : "never"}</div> |
| 1047 | + <div>Next run: {heartbeat.next_run_at ? formatTaskDateTime(heartbeat.next_run_at) : "n/a"}</div> |
| 1048 | + <div>Last tick: {heartbeat.last_tick_at ? formatTaskDateTime(heartbeat.last_tick_at) : "never"}</div> |
| 1049 | + <div>Last trigger: {heartbeat.last_triggered_at ? formatTaskDateTime(heartbeat.last_triggered_at) : "never"}</div> |
1043 | 1050 | <div>Cooldown: {heartbeat.cooldown_seconds || 0}s</div> |
1044 | 1051 | </div> |
1045 | 1052 | <div style={{ marginBottom: 20 }}> |
@@ -1083,7 +1090,7 @@ function HeartbeatDetailPanel({ heartbeat, ticks, onClose }) { |
1083 | 1090 | <div onClick={() => setSelectedTickId(tick.id)} style={{ display: "flex", justifyContent: "space-between", gap: 8, marginBottom: 6 }}> |
1084 | 1091 | <div style={{ fontSize: 12, fontWeight: 600, color: theme.text }}>{tick.decision_type || tick.status}</div> |
1085 | 1092 | <div style={{ fontSize: 11, color: theme.textDim, fontFamily: "monospace" }}> |
1086 | | - {tick.started_at ? new Date(tick.started_at).toLocaleString() : ""} |
| 1093 | + {tick.started_at ? formatTaskDateTime(tick.started_at) : ""} |
1087 | 1094 | </div> |
1088 | 1095 | </div> |
1089 | 1096 | {payload?.reason && ( |
@@ -1155,7 +1162,7 @@ function NewTaskModal({ onClose, onSubmit, initialData, mode = "create" }) { |
1155 | 1162 | cron_expr: initialData.cron_expr || "", |
1156 | 1163 | delay_seconds: initialData.delay_seconds || 60, |
1157 | 1164 | scheduled_at: initialData.next_run_at |
1158 | | - ? new Date(initialData.next_run_at).toISOString().slice(0, 16) |
| 1165 | + ? formatDateTimeLocalInput(initialData.next_run_at) |
1159 | 1166 | : "", |
1160 | 1167 | max_runs: initialData.max_runs || "", |
1161 | 1168 | tags: initialData.tags || "", |
@@ -1236,13 +1243,14 @@ function NewTaskModal({ onClose, onSubmit, initialData, mode = "create" }) { |
1236 | 1243 |
|
1237 | 1244 | // Handle scheduled_at: convert datetime-local to ISO timestamp |
1238 | 1245 | if (form.schedule_type === "scheduled_at") { |
1239 | | - const localDate = new Date(form.scheduled_at); |
1240 | | - if (!form.scheduled_at || isNaN(localDate.getTime())) { |
| 1246 | + const localDate = parseTaskDateTime(form.scheduled_at); |
| 1247 | + const serialized = serializeDateTimeLocalInput(form.scheduled_at); |
| 1248 | + if (!form.scheduled_at || !serialized || !localDate || isNaN(localDate.getTime())) { |
1241 | 1249 | setScheduledAtError("Please enter a valid date and time."); |
1242 | 1250 | return; |
1243 | 1251 | } |
1244 | 1252 | setScheduledAtError(""); |
1245 | | - data.next_run_at = localDate.toISOString(); |
| 1253 | + data.next_run_at = serialized; |
1246 | 1254 | } |
1247 | 1255 |
|
1248 | 1256 | onSubmit(data); |
@@ -1621,7 +1629,7 @@ function DetailPanel({ task, onClose, onRespond, onResume }) { |
1621 | 1629 | </h2> |
1622 | 1630 |
|
1623 | 1631 | <div style={{ fontSize: 11, color: theme.textDim, marginBottom: 24, fontFamily: "monospace" }}> |
1624 | | - ID: {task.id} · Created: {new Date(task.created_at).toLocaleString()} |
| 1632 | + ID: {task.id} · Created: {formatTaskDateTime(task.created_at)} |
1625 | 1633 | </div> |
1626 | 1634 |
|
1627 | 1635 | <Section title="Prompt"> |
@@ -1654,7 +1662,7 @@ function DetailPanel({ task, onClose, onRespond, onResume }) { |
1654 | 1662 | <InfoRow label="Schedule" value={task.schedule_type} /> |
1655 | 1663 | {task.cron_expr && <InfoRow label="Cron" value={task.cron_expr} />} |
1656 | 1664 | {task.delay_seconds && <InfoRow label="Delay" value={`${task.delay_seconds}s`} />} |
1657 | | - {task.next_run_at && <InfoRow label="Next Run" value={new Date(task.next_run_at).toLocaleString()} />} |
| 1665 | + {task.next_run_at && <InfoRow label="Next Run" value={formatTaskDateTime(task.next_run_at)} />} |
1658 | 1666 | <InfoRow label="Runs" value={`${task.run_count}${task.max_runs ? ` / ${task.max_runs}` : ""}`} /> |
1659 | 1667 | {task.dag_id && <InfoRow label="DAG" value={task.dag_id} />} |
1660 | 1668 | </Section> |
@@ -1924,7 +1932,7 @@ function DetailPanel({ task, onClose, onRespond, onResume }) { |
1924 | 1932 | {event.event_type} |
1925 | 1933 | </span> |
1926 | 1934 | <span style={{ color: theme.textDim, fontSize: 9 }}> |
1927 | | - {new Date(event.timestamp).toLocaleTimeString()} |
| 1935 | + {formatTaskTime(event.timestamp)} |
1928 | 1936 | </span> |
1929 | 1937 | </div> |
1930 | 1938 | <div style={{ |
|
0 commit comments