@linxin666/dsh-client-ui-task-board 0.1.20 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +82 -104
  3. package/README.zh.md +82 -104
  4. package/lib/client.js +574 -581
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +2144 -5
  7. package/lib/types/client/PluginSettingsCard.d.ts +7 -1
  8. package/lib/types/client/TaskBoardSettingsCard.d.ts +7 -4
  9. package/lib/types/client/board/TaskCard.d.ts +5 -2
  10. package/lib/types/client/host-api.d.ts +18 -0
  11. package/lib/types/client/locales.d.ts +10 -0
  12. package/lib/types/core/controller.d.ts +40 -15
  13. package/lib/types/core/schedule.d.ts +4 -2
  14. package/lib/types/core/tasks.d.ts +6 -6
  15. package/lib/types/core/use-cases/task-archive.d.ts +5 -4
  16. package/lib/types/core/use-cases/task-schedule.d.ts +3 -3
  17. package/lib/types/dsh-home.d.ts +16 -0
  18. package/lib/types/host-ledger.d.ts +42 -0
  19. package/lib/types/host-routes.d.ts +24 -0
  20. package/lib/types/host-runner.d.ts +30 -0
  21. package/lib/types/host-service.d.ts +41 -0
  22. package/lib/types/index.d.ts +17 -9
  23. package/lib/types/loopback.d.ts +24 -0
  24. package/lib/types/power-inhibitor.d.ts +73 -0
  25. package/lib/types/protocol.d.ts +72 -0
  26. package/package.json +12 -9
  27. package/src/client/PluginSettingsCard.tsx +10 -0
  28. package/src/client/TaskBoardSettingsCard.tsx +51 -4
  29. package/src/client/board/NewTaskModal.tsx +9 -6
  30. package/src/client/board/TaskBoard.tsx +21 -4
  31. package/src/client/board/TaskCard.tsx +24 -8
  32. package/src/client/board/TaskDetail.tsx +83 -50
  33. package/src/client/host-api.ts +95 -0
  34. package/src/client/index.ts +9 -78
  35. package/src/client/locales.ts +24 -4
  36. package/src/core/controller.ts +205 -24
  37. package/src/core/schedule.ts +20 -3
  38. package/src/core/store.ts +4 -8
  39. package/src/core/tasks.ts +6 -6
  40. package/src/core/use-cases/task-archive.ts +14 -5
  41. package/src/core/use-cases/task-schedule.ts +6 -5
  42. package/src/dsh-home.ts +36 -0
  43. package/src/host-ledger.ts +478 -0
  44. package/src/host-routes.ts +188 -0
  45. package/src/host-runner.ts +134 -0
  46. package/src/host-service.ts +209 -0
  47. package/src/index.ts +54 -11
  48. package/src/loopback.ts +63 -0
  49. package/src/power-inhibitor.ts +321 -0
  50. package/src/protocol.ts +213 -0
  51. package/lib/types/core/scheduler.d.ts +0 -70
  52. package/src/core/scheduler.ts +0 -161
@@ -0,0 +1,72 @@
1
+ import type { TaskUpdatePatch } from './core/use-cases/task-update.ts';
2
+ import { type NewTaskInput, type TaskRecord, type TaskStatus } from './core/tasks.ts';
3
+ export declare const TASK_BOARD_SCHEMA_VERSION: 2;
4
+ export declare const TASK_BOARD_API_PREFIX = "/api/task-board";
5
+ export type PowerPhase = 'disabled' | 'idle' | 'acquiring' | 'active' | 'error' | 'unsupported';
6
+ export interface TaskBoardPowerSnapshot {
7
+ platform: string;
8
+ phase: PowerPhase;
9
+ enabled: boolean;
10
+ runningSessions: number;
11
+ armedSchedules: number;
12
+ sessionStateKnown: boolean;
13
+ lastError?: string;
14
+ }
15
+ export interface TaskBoardSchedulerSnapshot {
16
+ timeZone: string;
17
+ /** Opaque identity of the current Host ledger generation. */
18
+ ledgerId?: string;
19
+ lastTickAt?: number;
20
+ error?: string;
21
+ }
22
+ export interface TaskBoardSnapshot {
23
+ schemaVersion: typeof TASK_BOARD_SCHEMA_VERSION;
24
+ revision: number;
25
+ tasks: TaskRecord[];
26
+ scheduler: TaskBoardSchedulerSnapshot;
27
+ power: TaskBoardPowerSnapshot;
28
+ }
29
+ export type TaskBoardAction = {
30
+ kind: 'import';
31
+ sourceId: string;
32
+ tasks: TaskRecord[];
33
+ } | {
34
+ kind: 'create';
35
+ id: string;
36
+ input: NewTaskInput;
37
+ } | {
38
+ kind: 'update';
39
+ taskId: string;
40
+ patch: TaskUpdatePatch;
41
+ } | {
42
+ kind: 'delete';
43
+ taskId: string;
44
+ } | {
45
+ kind: 'move';
46
+ taskId: string;
47
+ status: TaskStatus;
48
+ } | {
49
+ kind: 'archive';
50
+ taskId: string;
51
+ } | {
52
+ kind: 'restore';
53
+ taskId: string;
54
+ } | {
55
+ kind: 'set-schedule';
56
+ taskId: string;
57
+ patch: {
58
+ enabled?: boolean;
59
+ cron?: string;
60
+ };
61
+ } | {
62
+ kind: 'run';
63
+ taskId: string;
64
+ } | {
65
+ kind: 'rerun';
66
+ taskId: string;
67
+ };
68
+ export interface TaskBoardActionEnvelope {
69
+ requestId: string;
70
+ action: TaskBoardAction;
71
+ }
72
+ export declare function parseActionEnvelope(value: unknown): TaskBoardActionEnvelope | undefined;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-client-ui-task-board",
3
- "description": "Task board for the dsh web GUI: a sidebar entry plus a multi-column kanban view with local persistence, task details, and real execution through dsh sessions (session.prompt). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
4
- "version": "0.1.20",
3
+ "description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -42,15 +42,18 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@deepseek-ai/cordis": "^4.0.1",
45
- "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
46
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
47
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
48
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
49
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
50
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
51
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
45
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
46
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
47
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
48
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
49
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
50
+ "@deepseek-ai/dsh-host-apiproxy": "^0.1.0-rc.7",
51
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
52
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
53
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.7",
52
54
  "@types/react": "~18.3.1",
53
55
  "@types/react-dom": "^18.3.5",
56
+ "@types/node": "^22.20.0",
54
57
  "jsdom": "^25.0.0",
55
58
  "lightningcss": "1.32.0",
56
59
  "react": "^18.3.1",
@@ -56,6 +56,12 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
56
56
  * already provides the selection.
57
57
  */
58
58
  alwaysOpen?: boolean
59
+ /**
60
+ * Hide the save/discard footer: cards whose body applies its own changes
61
+ * immediately (an embedded external settings section) have no staged
62
+ * edits, so the footer would sit there permanently disabled.
63
+ */
64
+ hideFooter?: boolean
59
65
  /** The plugin's controls. */
60
66
  children: ReactNode
61
67
  }
@@ -140,6 +146,9 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
140
146
  <div className={css.body}>
141
147
  {!state.writable ? <p className={css.readOnly} role="status">{props.t('settings.readOnly')}</p> : null}
142
148
  {props.children}
149
+ {props.hideFooter === true
150
+ ? null
151
+ : (
143
152
  <div className={css.footer}>
144
153
  {state.failed
145
154
  ? (
@@ -165,6 +174,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
165
174
  {props.t(!state.saving ? 'settings.save' : 'settings.saving')}
166
175
  </button>
167
176
  </div>
177
+ )}
168
178
  </div>
169
179
  )
170
180
  : null}
@@ -1,12 +1,13 @@
1
1
  /**
2
- * The task-board settings card: whether the board announces itself in every
3
- * agent's system prompt. Registers into the `settings.plugin.item` slot the
4
- * plugin-configuration section renders, bound to the `task-board` settings
5
- * namespace.
2
+ * Task-board settings for availability, agent announcement, and optional Host
3
+ * idle-sleep protection. Registers into the `settings.plugin.item` slot the
4
+ * plugin-configuration section renders, bound to the `task-board` namespace.
6
5
  */
7
6
 
8
7
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
9
8
  import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
9
+ import { useEffect, useState } from 'react'
10
+ import type { TaskBoardPowerSnapshot } from '../protocol.ts'
10
11
  import { PluginSettingsCard, BooleanField } from './PluginSettingsCard.tsx'
11
12
  import { CardForm, booleanField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
12
13
 
@@ -16,6 +17,8 @@ export interface TaskBoardSettings {
16
17
  enabled?: boolean
17
18
  /** Whether the board announces itself in every agent's system prompt. */
18
19
  announceToAgent?: boolean
20
+ /** Prevent host idle sleep while sessions run or schedules are armed. */
21
+ preventIdleSleep?: boolean
19
22
  }
20
23
 
21
24
  /** What the task-board card renders. */
@@ -24,6 +27,8 @@ export interface TaskBoardSettingsCardState extends CardShell {
24
27
  enabled: CardFieldState
25
28
  /** System-prompt announcement flag. */
26
29
  announceToAgent: CardFieldState
30
+ /** Idle-system-sleep protection flag. */
31
+ preventIdleSleep: CardFieldState
27
32
  }
28
33
 
29
34
  /** The registration-side face the card's slot entry injects. */
@@ -44,6 +49,7 @@ export class TaskBoardSettingsCardController {
44
49
  this.form = new CardForm(scope, [
45
50
  booleanField('enabled'),
46
51
  booleanField('announceToAgent'),
52
+ booleanField('preventIdleSleep'),
47
53
  ])
48
54
  this.store = this.form.bind(() => this.projection())
49
55
  }
@@ -53,6 +59,7 @@ export class TaskBoardSettingsCardController {
53
59
  ...this.form.shell(),
54
60
  enabled: this.form.field('enabled'),
55
61
  announceToAgent: this.form.field('announceToAgent'),
62
+ preventIdleSleep: this.form.field('preventIdleSleep'),
56
63
  }
57
64
  }
58
65
 
@@ -88,6 +95,23 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
88
95
  const { t } = props
89
96
  const state = props.useTaskBoardSettingsCard(snapshot => snapshot)
90
97
  const disabled = !state.writable
98
+ const [power, setPower] = useState<TaskBoardPowerSnapshot | undefined>()
99
+ useEffect(() => {
100
+ let live = true
101
+ const refresh = async (): Promise<void> => {
102
+ try {
103
+ const response = await fetch('/api/task-board/state', { cache: 'no-store', credentials: 'same-origin' })
104
+ if (!response.ok) return
105
+ const snapshot = await response.json() as { power?: TaskBoardPowerSnapshot }
106
+ if (live) setPower(snapshot.power)
107
+ } catch {
108
+ // The settings form remains usable while the host status is reconnecting.
109
+ }
110
+ }
111
+ void refresh()
112
+ const timer = setInterval(() => { void refresh() }, 5_000)
113
+ return () => { live = false; clearInterval(timer) }
114
+ }, [])
91
115
  const fieldProps = {
92
116
  overriddenLabel: t('settings.overridden'),
93
117
  resetLabel: t('settings.reset'),
@@ -99,6 +123,7 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
99
123
  t={t}
100
124
  titleKey="settings.title"
101
125
  descriptionKey="settings.description"
126
+ defaultOpen={false}
102
127
  state={state}
103
128
  onSave={props.save}
104
129
  onDiscard={props.discard}
@@ -127,6 +152,28 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
127
152
  onEdit={(text) => { props.edit('announceToAgent', text) }}
128
153
  onReset={() => { props.resetField('announceToAgent') }}
129
154
  />
155
+ <BooleanField
156
+ id="settings-task-board-prevent-idle-sleep"
157
+ label={t('settings.preventIdleSleep')}
158
+ hint={t('settings.preventIdleSleepHint')}
159
+ inheritLabel={t('settings.inherit')}
160
+ onLabel={t('settings.on')}
161
+ offLabel={t('settings.off')}
162
+ {...fieldProps}
163
+ {...state.preventIdleSleep}
164
+ onEdit={(text) => { props.edit('preventIdleSleep', text) }}
165
+ onReset={() => { props.resetField('preventIdleSleep') }}
166
+ />
167
+ <p>
168
+ {t('settings.powerStatus', {
169
+ platform: power?.platform ?? t('settings.powerUnknown'),
170
+ phase: power?.phase ?? t('settings.powerUnknown'),
171
+ running: String(power?.runningSessions ?? 0),
172
+ schedules: String(power?.armedSchedules ?? 0),
173
+ })}
174
+ </p>
175
+ <p>{t('settings.powerBoundary')}</p>
176
+ {power?.lastError !== undefined && <p>{t('settings.powerError', { error: power.lastError })}</p>}
130
177
  </PluginSettingsCard>
131
178
  )
132
179
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * New-task modal: title + description + the prompt that execution will send.
3
- * Creates through the controller (which persists immediately).
3
+ * Creates through the Host and closes only after the Host confirms it.
4
4
  */
5
5
  import { useEffect, useState } from 'react'
6
6
  import type { BoardController } from '../../core/controller.ts'
@@ -22,6 +22,7 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
22
22
  const [scheduleCron, setScheduleCron] = useState('')
23
23
  const [scheduleError, setScheduleError] = useState<string | undefined>(undefined)
24
24
  const [error, setError] = useState<string | undefined>(undefined)
25
+ const [pending, setPending] = useState(false)
25
26
  const [options, setOptions] = useState(controller.getSnapshot().executionOptions)
26
27
 
27
28
  // The workspace list and preset roster arrive from the runtime after mount;
@@ -31,7 +32,7 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
31
32
  [controller],
32
33
  )
33
34
 
34
- const submit = (): void => {
35
+ const submit = async (): Promise<void> => {
35
36
  if (scheduleEnabled) {
36
37
  const cron = scheduleCron.trim()
37
38
  if (cron === '' || !isValidCron(cron)) {
@@ -39,7 +40,8 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
39
40
  return
40
41
  }
41
42
  }
42
- const task = controller.createTask({
43
+ setPending(true)
44
+ const task = await controller.createTaskConfirmed({
43
45
  title,
44
46
  description,
45
47
  prompt,
@@ -49,7 +51,8 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
49
51
  schedule: scheduleEnabled ? { enabled: true, cron: scheduleCron.trim() } : undefined,
50
52
  })
51
53
  if (task === undefined) {
52
- setError(t('new.required'))
54
+ setPending(false)
55
+ setError(controller.getSnapshot().transportError ?? t('new.required'))
53
56
  return
54
57
  }
55
58
  onClose()
@@ -66,7 +69,7 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
66
69
  className={css.modal}
67
70
  role="dialog"
68
71
  aria-label={t('board.new')}
69
- onSubmit={event => { event.preventDefault(); submit() }}
72
+ onSubmit={event => { event.preventDefault(); void submit() }}
70
73
  >
71
74
  <h2 className={css.modalTitle}>{t('board.new')}</h2>
72
75
 
@@ -205,7 +208,7 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
205
208
  <button type="button" className={css.ghostButton} onClick={onClose}>
206
209
  {t('new.cancel')}
207
210
  </button>
208
- <button type="submit" className={css.primaryButton}>
211
+ <button type="submit" className={css.primaryButton} disabled={pending}>
209
212
  {t('new.submit')}
210
213
  </button>
211
214
  </footer>
@@ -26,9 +26,9 @@ function matchesFilter(task: TaskRecord, filter: string): boolean {
26
26
  * re-renders only when its own task changes — not when a sibling card status,
27
27
  * the filter, or the selection moves.
28
28
  */
29
- const MemoTaskCard = memo(function MemoTaskCard({ task, onOpen }: { task: TaskRecord; onOpen: (id: string) => void }) {
29
+ const MemoTaskCard = memo(function MemoTaskCard({ task, pending, timeZone, onOpen }: { task: TaskRecord; pending: boolean; timeZone?: string; onOpen: (id: string) => void }) {
30
30
  const onClick = useCallback(() => { onOpen(task.id) }, [task.id, onOpen])
31
- return <TaskCard task={task} onClick={onClick} />
31
+ return <TaskCard task={task} pending={pending} timeZone={timeZone} onClick={onClick} />
32
32
  })
33
33
 
34
34
  /** Board component; subscribes to the controller snapshot. */
@@ -62,6 +62,14 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
62
62
  <span>{t('board.close')}</span>
63
63
  </button>
64
64
  <h2 className={css.boardTitle}>{t('board.title')}</h2>
65
+ {snapshot.host !== undefined && (
66
+ <span className={css.detailMeta}>
67
+ {t('board.hostMeta', {
68
+ revision: String(snapshot.host.revision),
69
+ timeZone: snapshot.host.scheduler.timeZone,
70
+ })}
71
+ </span>
72
+ )}
65
73
  <input
66
74
  className={css.search}
67
75
  type="search"
@@ -88,6 +96,15 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
88
96
  </button>
89
97
  </header>
90
98
 
99
+ {snapshot.transportError !== undefined && (
100
+ <div className={css.formError}>
101
+ {t('board.hostError', { error: snapshot.transportError })}{' '}
102
+ <button type="button" className={css.linkButton} onClick={() => { void controller.retryHostSync() }}>
103
+ {t('board.retryHost')}
104
+ </button>
105
+ </div>
106
+ )}
107
+
91
108
  <div className={css.columns}>
92
109
  {archiveView ? (
93
110
  <section className={css.column} data-status="archived">
@@ -97,7 +114,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
97
114
  </header>
98
115
  <div className={css.cards}>
99
116
  {visible.map(task => (
100
- <MemoTaskCard key={task.id} task={task} onOpen={openTask} />
117
+ <MemoTaskCard key={task.id} task={task} pending={snapshot.pendingTaskIds.includes(task.id)} timeZone={snapshot.host?.scheduler.timeZone} onOpen={openTask} />
101
118
  ))}
102
119
  {visible.length === 0 && <div className={css.columnEmpty}>{t('archive.empty')}</div>}
103
120
  </div>
@@ -114,7 +131,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
114
131
  </header>
115
132
  <div className={css.cards}>
116
133
  {tasks.map(task => (
117
- <MemoTaskCard key={task.id} task={task} onOpen={openTask} />
134
+ <MemoTaskCard key={task.id} task={task} pending={snapshot.pendingTaskIds.includes(task.id)} timeZone={snapshot.host?.scheduler.timeZone} onOpen={openTask} />
118
135
  ))}
119
136
  {tasks.length === 0 && <div className={css.columnEmpty}>{t('board.empty')}</div>}
120
137
  </div>
@@ -14,25 +14,40 @@ import { t } from '../locales.ts'
14
14
  import css from '../board.module.css'
15
15
 
16
16
  /** Compact relative/absolute time label. */
17
- export function formatTime(ms: number): string {
17
+ export function formatHostTimestamp(ms: number, timeZone?: string): string {
18
+ try {
19
+ return new Intl.DateTimeFormat(undefined, {
20
+ dateStyle: 'medium',
21
+ timeStyle: 'medium',
22
+ ...(timeZone === undefined ? {} : { timeZone }),
23
+ }).format(new Date(ms))
24
+ } catch {
25
+ return new Date(ms).toISOString()
26
+ }
27
+ }
28
+
29
+ export function formatTime(ms: number, timeZone?: string): string {
18
30
  const date = new Date(ms)
19
31
  const now = Date.now()
20
32
  const minutes = Math.floor((now - ms) / 60000)
21
33
  if (minutes < 1) return t('time.justNow')
22
34
  if (minutes < 60) return `${minutes}m`
23
35
  if (minutes < 60 * 24) return `${Math.floor(minutes / 60)}h`
36
+ if (timeZone !== undefined) return formatHostTimestamp(ms, timeZone)
24
37
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
25
38
  }
26
39
 
27
40
  /** One card in a column. */
28
- function TaskCardInner({ task, onClick }: { task: TaskRecord; onClick: () => void }) {
41
+ function TaskCardInner({ task, pending, timeZone, onClick }: { task: TaskRecord; pending: boolean; timeZone?: string; onClick: () => void }) {
29
42
  const latest = task.executions[task.executions.length - 1]
30
43
  const runs = task.executions.length
44
+ const archived = task.archivedAt !== undefined
31
45
  return (
32
46
  <button
33
47
  type="button"
34
48
  className={css.card}
35
- data-status={task.status}
49
+ data-status={archived ? 'archived' : task.status}
50
+ data-pending={pending || undefined}
36
51
  onClick={onClick}
37
52
  title={task.description !== '' ? task.description : task.title}
38
53
  >
@@ -40,27 +55,28 @@ function TaskCardInner({ task, onClick }: { task: TaskRecord; onClick: () => voi
40
55
  {task.description !== '' && <span className={css.cardExcerpt}>{task.description}</span>}
41
56
  <span className={css.cardMeta}>
42
57
  <span className={css.cardTime}>{t('board.updated')} {formatTime(task.updatedAt)}</span>
43
- {task.schedule?.enabled === true && (
58
+ {!archived && task.schedule?.enabled === true && (
44
59
  <span
45
60
  className={css.cardSchedule}
46
61
  title={task.schedule.nextRunAt !== undefined
47
- ? `${t('card.scheduled')} · ${new Date(task.schedule.nextRunAt).toLocaleString()}`
62
+ ? `${t('card.scheduled')} · ${formatHostTimestamp(task.schedule.nextRunAt, timeZone)}`
48
63
  : t('card.scheduled')}
49
64
  >
50
65
  {t('card.scheduled')}
51
66
  </span>
52
67
  )}
53
68
  {latest !== undefined && (
54
- <span className={css.cardRun} data-result={latest.result}>
69
+ <span className={css.cardRun} data-result={archived ? undefined : latest.result}>
55
70
  {runs} {t('board.runs')}
56
71
  </span>
57
72
  )}
58
73
  {latest?.sessionId !== undefined && (
59
74
  <span className={css.cardSession} title={latest.sessionId}>⌁</span>
60
75
  )}
61
- {task.status === 'running' && <span className={css.cardSpinner} aria-hidden="true" />}
76
+ {!archived && (task.status === 'running' || pending) && <span className={css.cardSpinner} aria-hidden="true" />}
62
77
  </span>
63
- {latest !== undefined && executionLabel(latest) === 'running' && (
78
+ {!archived && pending && <span className={css.cardRunningLabel}>{t('board.pending')}…</span>}
79
+ {!archived && latest !== undefined && executionLabel(latest) === 'running' && (
64
80
  <span className={css.cardRunningLabel}>{t('detail.result.running')}…</span>
65
81
  )}
66
82
  </button>