<script setup lang="ts">
import {
  AlertCircle,
  Calendar,
  CheckCircle2,
  ChevronRight,
  Clock3,
  History,
  Loader2,
  Pencil,
  Play,
  Plus,
  Power,
  PowerOff,
  RefreshCw,
  Save,
  ShieldAlert,
  TimerReset,
  Trash2,
  X,
} from '@lucide/vue'
import {
  computed,
  nextTick,
  onMounted,
  onUnmounted,
  reactive,
  ref,
  watch,
} from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import {
  useOpenClawCronDetail,
  useOpenClawCronJobs,
  useOpenClawCronMutations,
  useOpenClawCronRuns,
  type CreateOpenClawCronJobRequest,
} from '../api/openClawCron'
import AsyncStatePanel from '../components/mission-control/AsyncStatePanel.vue'
import { useOpenClawOverviewQuery } from '../api/openclawRuntime'
import { useAuthStore } from '../stores/auth'
import type {
  OpenClawCronJob,
  OpenClawCronJobDetail,
  OpenClawCronRun,
  OpenClawQueuedCronRun,
} from '../types/openclaw'

type DialogMode = 'closed' | 'detail' | 'create' | 'edit' | 'delete' | 'run'
type DetailTab = 'definition' | 'history'
type ScheduleKind = 'cron' | 'every' | 'at'
type PayloadKind = 'agentTurn' | 'systemEvent'

interface CronForm {
  name: string
  description: string
  enabled: boolean
  scheduleKind: ScheduleKind
  cronExpression: string
  timeZone: string
  intervalMinutes: number
  runAt: string
  sessionTarget: string
  wakeMode: 'now' | 'next-heartbeat'
  agentId: string
  sessionKey: string
  payloadKind: PayloadKind
  prompt: string
  model: string
  deliveryMode: 'none' | 'announce'
  deliveryChannel: string
  deliveryTarget: string
  bestEffort: boolean
  deleteAfterRun: boolean
  declarationKey: string
}

const overviewQuery = useOpenClawOverviewQuery()
const auth = useAuthStore()
const route = useRoute()
const router = useRouter()
const includeDisabled = ref(true)
const selectedJob = ref<OpenClawCronJob | null>(null)
const dialogMode = ref<DialogMode>('closed')
const detailTab = ref<DetailTab>('definition')
const localError = ref('')
const announcement = ref('')
const historyRunFilter = ref<string | null>(null)
const dialogRef = ref<HTMLElement | null>(null)
const previousFocus = ref<HTMLElement | null>(null)
const editorBaseline = ref<CronForm | null>(null)
const queuedCronRun = ref<OpenClawQueuedCronRun | null>(null)

const form = reactive<CronForm>(createEmptyForm())

const isOwner = computed(() => auth.user?.role === 'owner')
const capabilities = computed(() => overviewQuery.data.value?.capabilities ?? [])
const canCreate = computed(() => isOwner.value && capability('cron-create')?.available === true)
const canReadDetail = computed(() => isOwner.value && capability('cron-detail')?.available === true)
const canReadHistory = computed(() => capability('cron-history')?.available === true)
const selectedJobId = computed(() => selectedJob.value?.id ?? '')
const { query: jobsQuery, collection } = useOpenClawCronJobs(computed(() => ({
  includeDisabled: includeDisabled.value,
  limit: 50,
})))
const detailQuery = useOpenClawCronDetail(
  selectedJobId,
  computed(() =>
    canReadDetail.value
    && dialogMode.value !== 'closed'
    && dialogMode.value !== 'create',
  ),
)
const { query: cronRunsQuery, collection: cronRuns } = useOpenClawCronRuns(
  selectedJobId,
  historyRunFilter,
  computed(() =>
    canReadHistory.value
    && dialogMode.value !== 'closed'
    && detailTab.value === 'history',
  ),
)
const {
  createMutation,
  patchMutation,
  deleteMutation,
  runMutation,
} = useOpenClawCronMutations()
const jobs = computed(() => collection.value?.items ?? [])
const requestedJobId = computed(() =>
  typeof route.query.job === 'string' ? route.query.job : '',
)
const consumedJobLink = ref('')
const detail = computed(() => detailQuery.data.value ?? null)
const cronMutationLoading = computed(() =>
  createMutation.isPending.value
  || patchMutation.isPending.value
  || deleteMutation.isPending.value
  || runMutation.isPending.value,
)
const cronJobsLoading = computed(() => jobsQuery.isFetching.value)
const cronDetailLoading = computed(() => detailQuery.isFetching.value)
const cronRunsLoading = computed(() => cronRunsQuery.isFetching.value)
const cronMutationError = computed(() => {
  const error = createMutation.error.value
    ?? patchMutation.error.value
    ?? deleteMutation.error.value
    ?? runMutation.error.value
  return error instanceof Error ? error.message : ''
})
const cronJobsError = computed(() =>
  jobsQuery.error.value instanceof Error ? jobsQuery.error.value.message : '',
)
const cronDetailError = computed(() =>
  detailQuery.error.value instanceof Error ? detailQuery.error.value.message : '',
)
const cronRunsError = computed(() =>
  cronRunsQuery.error.value instanceof Error ? cronRunsQuery.error.value.message : '',
)
const managementBlocked = computed(() =>
  capabilities.value
    .filter(item => ['cron-create', 'cron-update', 'cron-delete', 'cron-run'].includes(item.id))
    .some(item => item.state === 'management_disabled'),
)
const mutationReason = computed(() => {
  if (!isOwner.value) return 'Owner role required.'
  return capabilities.value
    .find(item =>
      ['cron-create', 'cron-update', 'cron-delete', 'cron-run'].includes(item.id)
      && !item.available,
    )?.reason ?? null
})
const upcoming = computed(() =>
  jobs.value
    .filter(job => job.enabled && job.nextRunAt)
    .sort((left, right) => timestamp(left.nextRunAt) - timestamp(right.nextRunAt))
    .slice(0, 6),
)
const selectedName = computed(() => detail.value?.name ?? selectedJob.value?.name ?? 'Schedule')
const currentQueuedRun = computed(() => {
  const queued = queuedCronRun.value
  const jobId = detail.value?.id ?? selectedJob.value?.id
  return queued && queued.jobId === jobId ? queued : null
})
const trackedRun = computed(() => {
  const runId = currentQueuedRun.value?.runId
  if (!runId) return null
  return cronRuns.value?.items.find(run => run.runId === runId) ?? null
})
const scheduleEditable = computed(() =>
  !detail.value || ['cron', 'every', 'at'].includes(detail.value.schedule.kind),
)
const payloadEditable = computed(() =>
  !detail.value
  || (
    ['agentTurn', 'systemEvent'].includes(detail.value.payload.kind)
    && !detail.value.payload.inputConfigured
    && detail.value.payload.environmentKeys.length === 0
  ),
)
const deliveryEditable = computed(() =>
  !detail.value
  || (
    !detail.value.delivery?.completionDestination
    && !detail.value.delivery?.failureDestination
  ),
)

function capability(id: string) {
  return capabilities.value.find(item => item.id === id) ?? null
}

function createEmptyForm(): CronForm {
  return {
    name: '',
    description: '',
    enabled: true,
    scheduleKind: 'cron',
    cronExpression: '0 7 * * *',
    timeZone: 'Europe/Berlin',
    intervalMinutes: 60,
    runAt: '',
    sessionTarget: 'isolated',
    wakeMode: 'now',
    agentId: '',
    sessionKey: '',
    payloadKind: 'agentTurn',
    prompt: '',
    model: '',
    deliveryMode: 'none',
    deliveryChannel: '',
    deliveryTarget: '',
    bestEffort: true,
    deleteAfterRun: false,
    declarationKey: '',
  }
}

function resetForm(next: CronForm) {
  Object.assign(form, next)
  editorBaseline.value = structuredClone(next)
}

function timestamp(value: string | null) {
  if (!value) return Number.MAX_SAFE_INTEGER
  const parsed = Date.parse(value)
  return Number.isNaN(parsed) ? Number.MAX_SAFE_INTEGER : parsed
}

function formatDate(value: string | null) {
  if (!value) return 'Not scheduled'
  const date = new Date(value)
  if (Number.isNaN(date.getTime())) return value
  return date.toLocaleString('de-DE', {
    weekday: 'short',
    day: '2-digit',
    month: 'short',
    hour: '2-digit',
    minute: '2-digit',
  })
}

function formatDuration(value: number | null) {
  if (value === null) return '—'
  if (value < 1000) return `${value} ms`
  const seconds = value / 1000
  return seconds < 60 ? `${seconds.toFixed(1)} s` : `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`
}

function formatTokens(run: OpenClawCronRun) {
  if (run.totalTokens !== null) return run.totalTokens.toLocaleString('de-DE')
  const total = (run.inputTokens ?? 0) + (run.outputTokens ?? 0)
  return total > 0 ? total.toLocaleString('de-DE') : '—'
}

function statusTone(value: OpenClawCronJob | OpenClawCronRun | string) {
  const status = typeof value === 'string'
    ? value
    : 'lastRunStatus' in value
      ? value.lastRunStatus || value.status
      : value.status
  const normalized = status.toLocaleLowerCase()
  if (['failed', 'error', 'timed_out', 'lost'].includes(normalized)) return 'danger'
  if (['running', 'queued', 'started'].includes(normalized)) return 'info'
  if (['succeeded', 'success', 'completed', 'ok'].includes(normalized)) return 'success'
  if (typeof value !== 'string' && 'enabled' in value && !value.enabled) return 'muted'
  return 'warning'
}

function toLocalDateTime(value: string | null) {
  if (!value) return ''
  const date = new Date(value)
  if (Number.isNaN(date.getTime())) return ''
  const shifted = new Date(date.getTime() - date.getTimezoneOffset() * 60_000)
  return shifted.toISOString().slice(0, 16)
}

function editFormFromDetail(job: OpenClawCronJobDetail): CronForm {
  const supportedSchedule = ['cron', 'every', 'at'].includes(job.schedule.kind)
    ? job.schedule.kind as ScheduleKind
    : 'cron'
  const supportedPayload = ['agentTurn', 'systemEvent'].includes(job.payload.kind)
    ? job.payload.kind as PayloadKind
    : 'agentTurn'
  return {
    name: job.name,
    description: job.description ?? '',
    enabled: job.enabled,
    scheduleKind: supportedSchedule,
    cronExpression: job.schedule.expression ?? '0 7 * * *',
    timeZone: job.schedule.timeZone ?? '',
    intervalMinutes: Math.max(1, Math.round((job.schedule.everyMs ?? 3_600_000) / 60_000)),
    runAt: toLocalDateTime(job.schedule.at),
    sessionTarget: job.sessionTarget,
    wakeMode: job.wakeMode === 'next-heartbeat' ? 'next-heartbeat' : 'now',
    agentId: job.agentId ?? '',
    sessionKey: job.sessionKey ?? '',
    payloadKind: supportedPayload,
    prompt: job.payload.message ?? job.payload.text ?? '',
    model: job.payload.model ?? '',
    deliveryMode: job.delivery?.mode === 'announce' ? 'announce' : 'none',
    deliveryChannel: job.delivery?.channel ?? '',
    deliveryTarget: job.delivery?.target ?? '',
    bestEffort: job.delivery?.bestEffort ?? true,
    deleteAfterRun: job.deleteAfterRun,
    declarationKey: '',
  }
}

function buildSchedule() {
  if (form.scheduleKind === 'cron') {
    return {
      kind: 'cron',
      expr: form.cronExpression.trim(),
      ...(form.timeZone.trim() ? { tz: form.timeZone.trim() } : {}),
    }
  }
  if (form.scheduleKind === 'every') {
    return {
      kind: 'every',
      everyMs: Math.round(Number(form.intervalMinutes) * 60_000),
    }
  }
  return {
    kind: 'at',
    at: new Date(form.runAt).toISOString(),
  }
}

function detailSchedule(job: OpenClawCronJobDetail) {
  if (job.schedule.kind === 'cron') {
    return {
      kind: 'cron',
      expr: job.schedule.expression ?? '',
      ...(job.schedule.timeZone ? { tz: job.schedule.timeZone } : {}),
    }
  }
  if (job.schedule.kind === 'every') {
    return { kind: 'every', everyMs: job.schedule.everyMs }
  }
  if (job.schedule.kind === 'at') {
    return { kind: 'at', at: job.schedule.at }
  }
  return null
}

function buildPayload(existing?: OpenClawCronJobDetail) {
  if (form.payloadKind === 'systemEvent') {
    return { kind: 'systemEvent', text: form.prompt.trim() }
  }
  const payload: Record<string, unknown> = {
    kind: 'agentTurn',
    message: form.prompt.trim(),
    ...(form.model.trim() ? { model: form.model.trim() } : {}),
  }
  if (existing?.payload.kind === 'agentTurn') {
    if (existing.payload.fallbacks.length) payload.fallbacks = existing.payload.fallbacks
    if (existing.payload.thinking) payload.thinking = existing.payload.thinking
    if (existing.payload.timeoutSeconds !== null) payload.timeoutSeconds = existing.payload.timeoutSeconds
    if (existing.payload.allowUnsafeExternalContent !== null) {
      payload.allowUnsafeExternalContent = existing.payload.allowUnsafeExternalContent
    }
    if (existing.payload.lightContext !== null) payload.lightContext = existing.payload.lightContext
    if (existing.payload.toolsAllow.length) payload.toolsAllow = existing.payload.toolsAllow
    if (existing.payload.arguments.length) payload.argv = existing.payload.arguments
    if (existing.payload.workingDirectory) payload.cwd = existing.payload.workingDirectory
    if (existing.payload.noOutputTimeoutSeconds !== null) {
      payload.noOutputTimeoutSeconds = existing.payload.noOutputTimeoutSeconds
    }
    if (existing.payload.outputMaxBytes !== null) payload.outputMaxBytes = existing.payload.outputMaxBytes
  }
  return payload
}

function buildDelivery() {
  if (form.deliveryMode === 'none') return { mode: 'none' }
  return {
    mode: 'announce',
    channel: form.deliveryChannel.trim(),
    to: form.deliveryTarget.trim(),
    bestEffort: form.bestEffort,
  }
}

function fieldChanged(field: keyof CronForm) {
  return editorBaseline.value?.[field] !== form[field]
}

function validateForm() {
  if (!form.name.trim()) return 'A schedule name is required.'
  if (payloadEditable.value && !form.prompt.trim()) {
    return 'A safe agent message or system event is required.'
  }
  if (scheduleEditable.value && form.scheduleKind === 'cron' && !form.cronExpression.trim()) {
    return 'A cron expression is required.'
  }
  if (
    scheduleEditable.value
    &&
    form.scheduleKind === 'every'
    && (!Number.isFinite(Number(form.intervalMinutes)) || Number(form.intervalMinutes) <= 0)
  ) {
    return 'The interval must be greater than zero.'
  }
  if (
    scheduleEditable.value
    && form.scheduleKind === 'at'
    && Number.isNaN(new Date(form.runAt).getTime())
  ) {
    return 'A valid execution time is required.'
  }
  if (
    !['main', 'isolated', 'current'].includes(form.sessionTarget)
    && !form.sessionTarget.startsWith('session:')
  ) {
    return 'Session target must be main, isolated, current or session:<id>.'
  }
  if (
    deliveryEditable.value
    && form.deliveryMode === 'announce'
    && (!form.deliveryChannel.trim() || !form.deliveryTarget.trim())
  ) {
    return 'Announcement delivery requires both channel and target.'
  }
  return null
}

function buildCreateRequest(): CreateOpenClawCronJobRequest {
  return {
    name: form.name.trim(),
    description: form.description.trim() || null,
    enabled: form.enabled,
    schedule: buildSchedule(),
    sessionTarget: form.sessionTarget.trim(),
    wakeMode: form.wakeMode,
    payload: buildPayload(),
    agentId: form.agentId.trim() || null,
    sessionKey: form.sessionKey.trim() || null,
    deleteAfterRun: form.deleteAfterRun,
    delivery: buildDelivery(),
    declarationKey: form.declarationKey.trim() || null,
  }
}

function buildEditPatch(job: OpenClawCronJobDetail) {
  const patch: Record<string, unknown> = {}
  if (fieldChanged('name')) patch.name = form.name.trim()
  if (fieldChanged('description')) patch.description = form.description.trim() || null
  if (fieldChanged('enabled')) patch.enabled = form.enabled
  if (fieldChanged('agentId')) patch.agentId = form.agentId.trim() || null
  if (fieldChanged('sessionKey')) patch.sessionKey = form.sessionKey.trim() || null
  if (fieldChanged('sessionTarget')) patch.sessionTarget = form.sessionTarget.trim()
  if (fieldChanged('wakeMode')) patch.wakeMode = form.wakeMode
  if (fieldChanged('deleteAfterRun')) patch.deleteAfterRun = form.deleteAfterRun

  const scheduleChanged = [
    'scheduleKind',
    'cronExpression',
    'timeZone',
    'intervalMinutes',
    'runAt',
  ].some(field => fieldChanged(field as keyof CronForm))
  if (scheduleChanged && scheduleEditable.value) patch.schedule = buildSchedule()

  const payloadChanged = ['payloadKind', 'prompt', 'model']
    .some(field => fieldChanged(field as keyof CronForm))
  if (payloadChanged && payloadEditable.value) patch.payload = buildPayload(job)

  const deliveryChanged = [
    'deliveryMode',
    'deliveryChannel',
    'deliveryTarget',
    'bestEffort',
  ].some(field => fieldChanged(field as keyof CronForm))
  if (deliveryChanged && deliveryEditable.value) patch.delivery = buildDelivery()

  if (
    scheduleChanged
    && scheduleEditable.value
    && JSON.stringify(patch.schedule) === JSON.stringify(detailSchedule(job))
  ) {
    delete patch.schedule
  }
  return patch
}

function captureFocus() {
  previousFocus.value = document.activeElement instanceof HTMLElement
    ? document.activeElement
    : null
}

async function focusDialog() {
  await nextTick()
  dialogRef.value
    ?.querySelector<HTMLElement>('[data-autofocus], button:not(:disabled), input:not(:disabled)')
    ?.focus()
}

function resetCronMutationErrors() {
  createMutation.reset()
  patchMutation.reset()
  deleteMutation.reset()
  runMutation.reset()
}

function closeDialog() {
  if (cronMutationLoading.value) return
  dialogMode.value = 'closed'
  localError.value = ''
  historyRunFilter.value = null
  resetCronMutationErrors()
  if (typeof route.query.job === 'string') {
    const query = { ...route.query }
    delete query.job
    void router.replace({ query })
  }
}

async function openDetail(job: OpenClawCronJob, tab: DetailTab = 'definition') {
  captureFocus()
  selectedJob.value = job
  detailTab.value = !canReadDetail.value && canReadHistory.value ? 'history' : tab
  dialogMode.value = 'detail'
  localError.value = ''
  historyRunFilter.value = null
  resetCronMutationErrors()
  await focusDialog()

  const requests: Promise<unknown>[] = []
  if (canReadDetail.value) requests.push(detailQuery.refetch())
  if (detailTab.value === 'history' && canReadHistory.value) {
    requests.push(cronRunsQuery.refetch())
  }
  await Promise.allSettled(requests)
}

watch(
  [requestedJobId, jobs, canReadDetail, canReadHistory],
  ([jobId, items]) => {
    if (!jobId) {
      consumedJobLink.value = ''
      return
    }
    if (consumedJobLink.value === jobId) return
    const job = items.find(item => item.id === jobId)
    if (!job) return
    consumedJobLink.value = jobId
    void openDetail(job)
  },
  { immediate: true },
)

function openCreate() {
  captureFocus()
  selectedJob.value = null
  resetCronMutationErrors()
  resetForm(createEmptyForm())
  dialogMode.value = 'create'
  localError.value = ''
  void focusDialog()
}

function openEdit() {
  if (!detail.value) return
  resetForm(editFormFromDetail(detail.value))
  dialogMode.value = 'edit'
  localError.value = ''
  void focusDialog()
}

function openDeleteConfirmation() {
  if (!detail.value) return
  dialogMode.value = 'delete'
  localError.value = ''
  void focusDialog()
}

function openRunConfirmation() {
  if (!detail.value && !selectedJob.value) return
  dialogMode.value = 'run'
  localError.value = ''
  void focusDialog()
}

function openListRun(job: OpenClawCronJob) {
  captureFocus()
  selectedJob.value = job
  openRunConfirmation()
}

async function selectDetailTab(tab: DetailTab) {
  detailTab.value = tab
  if (tab === 'history' && canReadHistory.value && selectedJob.value && !cronRuns.value) {
    await cronRunsQuery.refetch().catch(() => undefined)
  }
}

async function submitEditor() {
  localError.value = validateForm() ?? ''
  if (localError.value) return

  try {
    if (dialogMode.value === 'create') {
      const result = await createMutation.mutateAsync(buildCreateRequest())
      const created = result.data
      if (!created && result.state === 'replayed') {
        announcement.value = `${result.message} The schedule list was refreshed; the metadata-only audit does not retain the original created resource.`
        dialogMode.value = 'closed'
        localError.value = ''
        await focusDialog()
        return
      }
      if (!created) throw new Error('OpenClaw returned no created schedule.')
      selectedJob.value = {
        id: created.id,
        name: created.name,
        description: created.description,
        schedule: created.schedule.expression ?? created.schedule.kind,
        timeZone: created.schedule.timeZone,
        enabled: created.enabled,
        status: 'idle',
        agentId: created.agentId,
        sessionKey: created.sessionKey,
        nextRunAt: created.nextRunAt,
        lastRunAt: created.lastRunAt,
        lastRunStatus: created.lastRunStatus,
        lastError: created.lastError,
        canRun: created.canRun,
        resourceHash: created.resourceHash,
      }
      announcement.value = result.message
      await overviewQuery.refetch()
    } else {
      if (!detail.value) return
      const patch = buildEditPatch(detail.value)
      if (Object.keys(patch).length === 0) {
        localError.value = 'No supported changes are pending.'
        return
      }
      const result = await patchMutation.mutateAsync({
        jobId: detail.value.id,
        patch,
        expectedHash: detail.value.resourceHash,
      })
      announcement.value = result.message
      await overviewQuery.refetch()
    }
    dialogMode.value = 'detail'
    detailTab.value = 'definition'
    localError.value = ''
    await focusDialog()
  } catch (error) {
    localError.value = error instanceof Error ? error.message : 'Schedule could not be saved.'
  }
}

async function toggleEnabled() {
  if (!detail.value) return
  localError.value = ''
  try {
    const result = await patchMutation.mutateAsync({
      jobId: detail.value.id,
      patch: { enabled: !detail.value.enabled },
      expectedHash: detail.value.resourceHash,
    })
    announcement.value = result.message
    await overviewQuery.refetch()
  } catch (error) {
    localError.value = error instanceof Error ? error.message : 'Schedule state could not be changed.'
  }
}

async function deleteJob() {
  if (!detail.value) return
  localError.value = ''
  try {
    const name = detail.value.name
    const result = await deleteMutation.mutateAsync({
      jobId: detail.value.id,
      expectedHash: detail.value.resourceHash,
    })
    announcement.value = `${name}: ${result.message}`
    await overviewQuery.refetch()
    dialogMode.value = 'closed'
  } catch (error) {
    localError.value = error instanceof Error ? error.message : 'Schedule could not be deleted.'
  }
}

async function runJob() {
  const jobId = detail.value?.id ?? selectedJob.value?.id
  if (!jobId) return
  localError.value = ''
  try {
    const result = await runMutation.mutateAsync({
      jobId,
      expectedHash: detail.value?.resourceHash ?? selectedJob.value?.resourceHash ?? '',
    })
    await overviewQuery.refetch()
    const queued = result.data
    announcement.value = queued?.runId
      ? `Run ${queued.runId} was queued. Its outcome remains pending until cron history reports it.`
      : 'The run was queued. Its outcome remains pending until cron history reports it.'
    dialogMode.value = 'detail'
    detailTab.value = 'history'
    historyRunFilter.value = queued?.runId ?? null
    queuedCronRun.value = {
      jobId: queued?.jobId || jobId,
      runId: queued?.runId ?? null,
      enqueuedAt: new Date().toISOString(),
    }
    if (canReadHistory.value) {
      await cronRunsQuery.refetch().catch(() => undefined)
    }
    await focusDialog()
  } catch (error) {
    localError.value = error instanceof Error ? error.message : 'Cron run could not be queued.'
  }
}

async function refreshHistory(showAll = false) {
  const jobId = detail.value?.id ?? selectedJob.value?.id
  if (!jobId) return
  if (showAll) historyRunFilter.value = null
  await cronRunsQuery.refetch().catch(() => undefined)
}

async function loadMoreHistory() {
  const jobId = detail.value?.id ?? selectedJob.value?.id
  if (!jobId || !cronRuns.value?.nextCursor || historyRunFilter.value) return
  await cronRunsQuery.fetchNextPage().catch(() => undefined)
}

async function refreshJobs() {
  await Promise.allSettled([
    jobsQuery.refetch(),
    overviewQuery.refetch(),
  ])
}

async function toggleDisabledFilter() {
  includeDisabled.value = !includeDisabled.value
  await nextTick()
  await jobsQuery.refetch().catch(() => undefined)
}

async function loadMoreJobs() {
  if (!collection.value?.nextCursor) return
  await jobsQuery.fetchNextPage().catch(() => undefined)
}

function onWindowKeydown(event: KeyboardEvent) {
  if (event.key === 'Escape' && dialogMode.value !== 'closed') {
    event.preventDefault()
    closeDialog()
  }
}

function trapDialogFocus(event: KeyboardEvent) {
  if (event.key !== 'Tab' || !dialogRef.value) return
  const focusable = [...dialogRef.value.querySelectorAll<HTMLElement>(
    'button:not(:disabled), a[href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])',
  )].filter(item => item.offsetParent !== null)
  if (focusable.length === 0) return
  const first = focusable[0]
  const last = focusable[focusable.length - 1]
  if (event.shiftKey && document.activeElement === first) {
    event.preventDefault()
    last.focus()
  } else if (!event.shiftKey && document.activeElement === last) {
    event.preventDefault()
    first.focus()
  }
}

watch(dialogMode, async (next, previous) => {
  if (next !== 'closed') return
  if (previous !== 'closed') {
    await nextTick()
    previousFocus.value?.focus()
  }
})

onMounted(() => {
  window.addEventListener('keydown', onWindowKeydown)
})

onUnmounted(() => {
  window.removeEventListener('keydown', onWindowKeydown)
})
</script>

<template>
  <div class="calendar-page nexus-page nexus-page--workspace">
    <header class="nexus-page-header">
      <div class="nexus-page-header__identity">
        <div class="nexus-page-header__icon"><Calendar :size="20" aria-hidden="true" /></div>
        <div>
          <span class="eyebrow">OPENCLAW SCHEDULER</span>
          <h1>Calendar</h1>
          <p class="page-subtitle">Create, inspect and operate schedules without duplicating OpenClaw state.</p>
        </div>
      </div>
      <div class="header-actions">
        <button
          type="button"
          class="nexus-button"
          :disabled="cronJobsLoading || overviewQuery.isFetching.value"
          @click="refreshJobs"
        >
          <Loader2 v-if="cronJobsLoading || overviewQuery.isFetching.value" :size="15" class="spin" aria-hidden="true" />
          <RefreshCw v-else :size="15" aria-hidden="true" />
          Refresh
        </button>
        <button
          type="button"
          class="nexus-button nexus-button--primary"
          :disabled="!canCreate"
          :title="canCreate ? undefined : mutationReason || 'Cron creation is unavailable.'"
          @click="openCreate"
        >
          <Plus :size="15" aria-hidden="true" />
          New schedule
        </button>
      </div>
    </header>

    <section
      v-if="queuedCronRun"
      class="queued-banner nexus-panel"
      aria-labelledby="queued-run-title"
    >
      <span class="queued-icon"><TimerReset :size="18" aria-hidden="true" /></span>
      <div>
        <span class="eyebrow">RUN TRACKING</span>
        <h2 id="queued-run-title">OpenClaw accepted a run request</h2>
        <p>
          <code>{{ queuedCronRun.runId || 'Run ID pending' }}</code>
          is queued. Only the run history can confirm success or failure.
        </p>
      </div>
      <button
        v-if="jobs.find(job => job.id === queuedCronRun?.jobId)"
        type="button"
        class="nexus-button"
        @click="openDetail(jobs.find(job => job.id === queuedCronRun?.jobId)!, 'history')"
      >
        Track run
        <ChevronRight :size="14" aria-hidden="true" />
      </button>
    </section>

    <AsyncStatePanel
      v-if="cronJobsLoading && !collection"
      state="loading"
      title="OpenClaw-Zeitpläne werden geladen"
    />
    <AsyncStatePanel
      v-else-if="cronJobsError && !collection"
      state="error"
      title="Scheduler nicht verfügbar"
      :problem="jobsQuery.error.value"
      action-label="OpenClaw-Diagnose öffnen"
      @action="router.push('/settings')"
    />
    <AsyncStatePanel
      v-else-if="collection && collection.state !== 'ready'"
      :state="['disconnected', 'error', 'failed'].includes(collection.state) ? 'offline' : 'partial'"
      :title="`Scheduler: ${collection.state}`"
      :message="collection.recovery || collection.message || 'OpenClaw hat keinen vollständigen Cron-Katalog geliefert.'"
      action-label="Verbindung prüfen"
      @action="router.push('/settings')"
    />

    <template v-else>
      <section class="calendar-layout">
        <section class="calendar-panel nexus-panel" aria-labelledby="upcoming-title">
          <header class="panel-heading">
            <div>
              <span class="eyebrow">NEXT UP</span>
              <h2 id="upcoming-title">Upcoming executions</h2>
            </div>
            <span class="nexus-status">{{ upcoming.length }}</span>
          </header>
          <div v-if="upcoming.length" class="upcoming-list">
            <button
              v-for="job in upcoming"
              :key="job.id"
              type="button"
              class="upcoming-item"
              :aria-label="`Open ${job.name}`"
              @click="openDetail(job)"
            >
              <span class="upcoming-icon"><Clock3 :size="15" aria-hidden="true" /></span>
              <span class="upcoming-copy">
                <strong>{{ job.name }}</strong>
                <time :datetime="job.nextRunAt || undefined">{{ formatDate(job.nextRunAt) }}</time>
                <code>{{ job.schedule }}<template v-if="job.timeZone"> · {{ job.timeZone }}</template></code>
              </span>
              <ChevronRight :size="15" aria-hidden="true" />
            </button>
          </div>
          <div v-else class="compact-empty">
            <Calendar :size="18" aria-hidden="true" />
            <p>No enabled jobs currently report a next execution.</p>
          </div>
        </section>

        <section class="calendar-panel nexus-panel" aria-labelledby="jobs-title">
          <header class="panel-heading jobs-heading">
            <div>
              <span class="eyebrow">OPENCLAW SOURCE OF TRUTH</span>
              <h2 id="jobs-title">Scheduled jobs</h2>
            </div>
            <div class="list-controls">
              <button
                type="button"
                class="filter-toggle"
                :aria-pressed="includeDisabled"
                :disabled="cronJobsLoading"
                @click="toggleDisabledFilter"
              >
                <span class="filter-indicator" aria-hidden="true"></span>
                Include disabled
              </button>
              <span class="nexus-status">{{ jobs.length }}</span>
            </div>
          </header>

          <p v-if="cronJobsError" class="inline-error" role="alert">{{ cronJobsError }}</p>

          <div v-if="jobs.length" class="job-list">
            <article v-for="job in jobs" :key="job.id" class="job-row">
              <span class="job-state" :class="statusTone(job)">
                <CheckCircle2 v-if="statusTone(job) === 'success'" :size="16" aria-hidden="true" />
                <AlertCircle v-else-if="statusTone(job) === 'danger'" :size="16" aria-hidden="true" />
                <TimerReset v-else :size="16" aria-hidden="true" />
              </span>
              <div class="job-copy">
                <div class="job-title-row">
                  <strong>{{ job.name }}</strong>
                  <span
                    class="nexus-status"
                    :class="job.enabled ? 'nexus-status--success' : ''"
                  >{{ job.enabled ? 'Enabled' : 'Disabled' }}</span>
                </div>
                <p v-if="job.description">{{ job.description }}</p>
                <div class="job-identifiers">
                  <code>{{ job.schedule }}</code>
                  <span v-if="job.timeZone">{{ job.timeZone }}</span>
                  <RouterLink
                    v-if="job.agentId"
                    class="entity-link"
                    :to="{ name: 'AgentDetail', params: { id: job.agentId } }"
                  >
                    Agent {{ job.agentId }}
                  </RouterLink>
                  <span v-if="job.sessionKey">Session {{ job.sessionKey }}</span>
                </div>
              </div>
              <div class="job-timing">
                <span>Next <strong>{{ formatDate(job.nextRunAt) }}</strong></span>
                <span>Last <strong>{{ formatDate(job.lastRunAt) }}</strong></span>
                <span v-if="job.lastError" class="job-error">{{ job.lastError }}</span>
              </div>
              <div class="job-actions">
                <button
                  type="button"
                  class="nexus-button"
                  :aria-label="`${canReadDetail ? 'Open details for' : 'Open run history for'} ${job.name}`"
                  :disabled="!canReadDetail && !canReadHistory"
                  @click="openDetail(job, canReadDetail ? 'definition' : 'history')"
                >
                  <History v-if="!canReadDetail" :size="14" aria-hidden="true" />
                  <ChevronRight v-else :size="14" aria-hidden="true" />
                  {{ canReadDetail ? 'Details' : 'History' }}
                </button>
                <button
                  type="button"
                  class="icon-button"
                  :disabled="!isOwner || !job.canRun || !job.resourceHash || cronMutationLoading"
                  :title="!isOwner ? 'Owner role required.' : capability('cron-run')?.reason || `Run ${job.name}`"
                  :aria-label="`Run ${job.name} now`"
                  @click="openListRun(job)"
                >
                  <Play :size="15" aria-hidden="true" />
                </button>
              </div>
            </article>

            <button
              v-if="collection?.nextCursor"
              type="button"
              class="nexus-button load-more"
              :disabled="cronJobsLoading"
              @click="loadMoreJobs"
            >
              <Loader2 v-if="cronJobsLoading" :size="14" class="spin" aria-hidden="true" />
              Load more schedules
            </button>
          </div>
          <div v-else class="compact-empty">
            <Calendar :size="18" aria-hidden="true" />
            <p>{{ includeDisabled ? 'OpenClaw returned no scheduled jobs.' : 'No enabled schedules were found.' }}</p>
          </div>
        </section>
      </section>

      <section
        v-if="managementBlocked || !isOwner || mutationReason"
        class="nexus-state capability-note"
        :class="{ 'nexus-state--error': !isOwner }"
      >
        <ShieldAlert :size="18" aria-hidden="true" />
        <div>
          <h2>{{ isOwner ? 'Cron management is read-only' : 'Owner access required' }}</h2>
          <p>
            {{ mutationReason || 'You can inspect schedules and history, but only an owner can change or run them.' }}
          </p>
        </div>
        <RouterLink v-if="isOwner && managementBlocked" class="nexus-button" to="/settings">
          Open Setup Center
        </RouterLink>
      </section>
    </template>

    <p class="nexus-visually-hidden" aria-live="polite">{{ announcement }}</p>

    <Teleport to="body">
      <div
        v-if="dialogMode !== 'closed'"
        class="cron-dialog-overlay"
        @click.self="closeDialog"
      >
        <section
          ref="dialogRef"
          class="cron-dialog"
          :class="{ 'cron-dialog--compact': ['delete', 'run'].includes(dialogMode) }"
          role="dialog"
          aria-modal="true"
          aria-labelledby="cron-dialog-title"
          @keydown="trapDialogFocus"
        >
          <header class="dialog-header">
            <div>
              <span class="eyebrow">
                {{ dialogMode === 'create' ? 'NEW OPENCLAW SCHEDULE' : dialogMode === 'edit' ? 'EDIT WITH HASH GUARD' : 'OPENCLAW CRON' }}
              </span>
              <h2 id="cron-dialog-title">
                <template v-if="dialogMode === 'create'">Create schedule</template>
                <template v-else-if="dialogMode === 'edit'">Edit {{ selectedName }}</template>
                <template v-else-if="dialogMode === 'delete'">Delete {{ selectedName }}?</template>
                <template v-else-if="dialogMode === 'run'">Run {{ selectedName }} now?</template>
                <template v-else>{{ selectedName }}</template>
              </h2>
            </div>
            <button
              type="button"
              class="dialog-close"
              aria-label="Close schedule dialog"
              :disabled="cronMutationLoading"
              @click="closeDialog"
            >
              <X :size="18" aria-hidden="true" />
            </button>
          </header>

          <div v-if="dialogMode === 'detail'" class="dialog-body detail-body">
            <div class="detail-tabs" role="tablist" aria-label="Schedule detail sections">
              <button
                v-if="canReadDetail"
                type="button"
                role="tab"
                :aria-selected="detailTab === 'definition'"
                :class="{ active: detailTab === 'definition' }"
                @click="selectDetailTab('definition')"
              >
                Definition
              </button>
              <button
                v-if="canReadHistory"
                type="button"
                role="tab"
                :aria-selected="detailTab === 'history'"
                :class="{ active: detailTab === 'history' }"
                @click="selectDetailTab('history')"
              >
                Run history
              </button>
            </div>

            <template v-if="detailTab === 'definition'">
              <div v-if="cronDetailLoading" class="dialog-state" role="status">
                <Loader2 :size="18" class="spin" aria-hidden="true" />
                Loading the live definition…
              </div>
              <div v-else-if="cronDetailError" class="dialog-state error" role="alert">
                <AlertCircle :size="18" aria-hidden="true" />
                <p>{{ cronDetailError }}</p>
              </div>
              <template v-else-if="detail">
                <section class="detail-summary">
                  <div>
                    <span class="eyebrow">STATE</span>
                    <span
                      class="nexus-status"
                      :class="detail.enabled ? 'nexus-status--success' : ''"
                    >{{ detail.enabled ? 'Enabled' : 'Disabled' }}</span>
                  </div>
                  <div><span class="eyebrow">NEXT RUN</span><strong>{{ formatDate(detail.nextRunAt) }}</strong></div>
                  <div><span class="eyebrow">LAST RUN</span><strong>{{ formatDate(detail.lastRunAt) }}</strong></div>
                  <div><span class="eyebrow">RESOURCE HASH</span><code>{{ detail.resourceHash }}</code></div>
                </section>

                <p v-if="detail.description" class="detail-description">{{ detail.description }}</p>
                <p v-if="detail.lastError" class="inline-error" role="alert">{{ detail.lastError }}</p>

                <div class="definition-grid">
                  <section class="definition-card">
                    <span class="eyebrow">SCHEDULE</span>
                    <h3>{{ detail.schedule.kind }}</h3>
                    <dl>
                      <div v-if="detail.schedule.expression"><dt>Expression</dt><dd><code>{{ detail.schedule.expression }}</code></dd></div>
                      <div v-if="detail.schedule.timeZone"><dt>Timezone</dt><dd>{{ detail.schedule.timeZone }}</dd></div>
                      <div v-if="detail.schedule.everyMs !== null"><dt>Interval</dt><dd>{{ detail.schedule.everyMs / 60_000 }} minutes</dd></div>
                      <div v-if="detail.schedule.at"><dt>At</dt><dd>{{ formatDate(detail.schedule.at) }}</dd></div>
                      <div v-if="detail.schedule.command"><dt>Command</dt><dd><code>{{ detail.schedule.command }}</code></dd></div>
                    </dl>
                  </section>
                  <section class="definition-card">
                    <span class="eyebrow">AGENT PAYLOAD</span>
                    <h3>{{ detail.payload.kind }}</h3>
                    <p>{{ detail.payload.message || detail.payload.text || 'No readable message.' }}</p>
                    <dl>
                      <div v-if="detail.agentId">
                        <dt>Agent</dt>
                        <dd>
                          <RouterLink
                            class="entity-link"
                            :to="{ name: 'AgentDetail', params: { id: detail.agentId } }"
                            @click="closeDialog"
                          >
                            {{ detail.agentId }}
                          </RouterLink>
                        </dd>
                      </div>
                      <div><dt>Session</dt><dd>{{ detail.sessionTarget }}</dd></div>
                      <div><dt>Wake</dt><dd>{{ detail.wakeMode }}</dd></div>
                      <div v-if="detail.payload.model"><dt>Model</dt><dd>{{ detail.payload.model }}</dd></div>
                    </dl>
                  </section>
                  <section class="definition-card">
                    <span class="eyebrow">DELIVERY</span>
                    <h3>{{ detail.delivery?.mode || 'none' }}</h3>
                    <dl v-if="detail.delivery">
                      <div v-if="detail.delivery.channel"><dt>Channel</dt><dd>{{ detail.delivery.channel }}</dd></div>
                      <div v-if="detail.delivery.target"><dt>Target</dt><dd class="sensitive-value">{{ detail.delivery.target }}</dd></div>
                      <div v-if="detail.delivery.accountId"><dt>Account</dt><dd>{{ detail.delivery.accountId }}</dd></div>
                      <div><dt>Best effort</dt><dd>{{ detail.delivery.bestEffort ? 'Yes' : 'No' }}</dd></div>
                    </dl>
                    <p v-else>No delivery target configured.</p>
                  </section>
                  <section class="definition-card">
                    <span class="eyebrow">LIFECYCLE</span>
                    <h3>{{ detail.deleteAfterRun ? 'One-time' : 'Persistent' }}</h3>
                    <dl>
                      <div><dt>Created</dt><dd>{{ formatDate(detail.createdAt) }}</dd></div>
                      <div><dt>Updated</dt><dd>{{ formatDate(detail.updatedAt) }}</dd></div>
                      <div v-if="detail.trigger"><dt>Trigger</dt><dd><code>{{ detail.trigger.script }}</code></dd></div>
                      <div v-if="detail.failureAlert"><dt>Failure alert</dt><dd>After {{ detail.failureAlert.after ?? 'configured' }}</dd></div>
                    </dl>
                  </section>
                </div>
              </template>
            </template>

            <template v-else>
              <section v-if="currentQueuedRun" class="tracked-run" aria-live="polite">
                <TimerReset :size="17" aria-hidden="true" />
                <div>
                  <strong v-if="trackedRun">
                    Run {{ currentQueuedRun.runId }}: {{ trackedRun.status }}
                  </strong>
                  <strong v-else>Waiting for run history</strong>
                  <p v-if="trackedRun">
                    OpenClaw history now reports this run; this status is authoritative.
                  </p>
                  <p v-else>
                    {{ currentQueuedRun.runId || 'OpenClaw returned no run ID.' }}
                    remains queued until <code>cron.runs</code> reports an outcome.
                  </p>
                </div>
              </section>

              <div class="history-toolbar">
                <div>
                  <span class="eyebrow">AUTHORITATIVE HISTORY</span>
                  <h3>{{ historyRunFilter ? `Tracking ${historyRunFilter}` : 'Recent runs' }}</h3>
                </div>
                <div>
                  <button
                    v-if="historyRunFilter"
                    type="button"
                    class="nexus-button"
                    :disabled="cronRunsLoading"
                    @click="refreshHistory(true)"
                  >
                    Show all
                  </button>
                  <button
                    type="button"
                    class="nexus-button"
                    :disabled="cronRunsLoading"
                    @click="refreshHistory(false)"
                  >
                    <Loader2 v-if="cronRunsLoading" :size="14" class="spin" aria-hidden="true" />
                    <RefreshCw v-else :size="14" aria-hidden="true" />
                    Refresh
                  </button>
                </div>
              </div>

              <p class="contract-note">
                <strong>Result links:</strong>
                <code>cron.runs</code> currently supplies an OpenClaw runtime run ID,
                but no Nexus durable Run or Task reference. Nexus shows the authoritative
                history without guessing a detail route.
              </p>

              <div v-if="cronRunsLoading && !cronRuns" class="dialog-state" role="status">
                <Loader2 :size="18" class="spin" aria-hidden="true" />
                Loading run history…
              </div>
              <div v-else-if="cronRunsError" class="dialog-state error" role="alert">
                <AlertCircle :size="18" aria-hidden="true" />
                <p>{{ cronRunsError }}</p>
              </div>
              <div
                v-else-if="cronRuns && cronRuns.state !== 'ready'"
                class="dialog-state error"
                role="status"
              >
                <AlertCircle :size="18" aria-hidden="true" />
                <p>{{ cronRuns.message || `Cron history is ${cronRuns.state}.` }}</p>
                <p v-if="cronRuns.recovery">{{ cronRuns.recovery }}</p>
              </div>
              <div v-else-if="cronRuns?.items.length" class="run-list">
                <article
                  v-for="run in cronRuns.items"
                  :key="run.id"
                  class="run-row"
                  :class="{ tracked: run.runId && run.runId === currentQueuedRun?.runId }"
                >
                  <span class="run-status" :class="statusTone(run)">
                    <CheckCircle2 v-if="statusTone(run) === 'success'" :size="15" aria-hidden="true" />
                    <AlertCircle v-else-if="statusTone(run) === 'danger'" :size="15" aria-hidden="true" />
                    <TimerReset v-else :size="15" aria-hidden="true" />
                  </span>
                  <div class="run-copy">
                    <div>
                      <strong>{{ run.status }}</strong>
                      <code>{{ run.runId || run.id }}</code>
                    </div>
                    <p v-if="run.summary">{{ run.summary }}</p>
                    <p v-if="run.error || run.errorReason" class="run-error">{{ run.error || run.errorReason }}</p>
                    <p v-if="run.deliveryError" class="run-error">{{ run.deliveryError }}</p>
                    <details v-if="run.diagnostics.length">
                      <summary>{{ run.diagnostics.length }} diagnostic entries</summary>
                      <ul>
                        <li v-for="(diagnostic, index) in run.diagnostics" :key="`${run.id}-${index}`">
                          <strong>{{ diagnostic.severity }}</strong>
                          {{ diagnostic.message }}
                        </li>
                      </ul>
                    </details>
                  </div>
                  <dl class="run-meta">
                    <div><dt>Occurred</dt><dd>{{ formatDate(run.occurredAt || run.runAt) }}</dd></div>
                    <div><dt>Duration</dt><dd>{{ formatDuration(run.durationMs) }}</dd></div>
                    <div><dt>Tokens</dt><dd>{{ formatTokens(run) }}</dd></div>
                    <div v-if="run.deliveryStatus"><dt>Delivery</dt><dd>{{ run.deliveryStatus }}</dd></div>
                  </dl>
                </article>
                <button
                  v-if="cronRuns.nextCursor && !historyRunFilter"
                  type="button"
                  class="nexus-button load-more"
                  :disabled="cronRunsLoading"
                  @click="loadMoreHistory"
                >
                  Load older runs
                </button>
              </div>
              <div v-else class="dialog-state empty">
                <History :size="18" aria-hidden="true" />
                <p>
                  {{ historyRunFilter ? 'This queued run is not present in OpenClaw history yet.' : 'OpenClaw returned no run history for this schedule.' }}
                </p>
              </div>
            </template>

            <p v-if="localError" class="inline-error" role="alert">{{ localError }}</p>
          </div>

          <form
            v-else-if="dialogMode === 'create' || dialogMode === 'edit'"
            class="dialog-body cron-form"
            @submit.prevent="submitEditor"
          >
            <section class="form-section">
              <div class="form-section-heading">
                <div><span class="eyebrow">IDENTITY</span><h3>Schedule intent</h3></div>
                <label class="checkbox-field">
                  <input v-model="form.enabled" type="checkbox" />
                  Enabled
                </label>
              </div>
              <div class="form-grid">
                <label class="form-field form-field--wide">
                  <span>Name</span>
                  <input v-model="form.name" data-autofocus maxlength="160" autocomplete="off" required />
                </label>
                <label class="form-field form-field--wide">
                  <span>Description</span>
                  <textarea v-model="form.description" rows="2" maxlength="1000"></textarea>
                </label>
                <label v-if="dialogMode === 'create'" class="form-field form-field--wide">
                  <span>Declaration key <small>optional, for idempotent declarative setup</small></span>
                  <input v-model="form.declarationKey" maxlength="200" autocomplete="off" placeholder="nexus.morning-brief" />
                </label>
              </div>
            </section>

            <section class="form-section">
              <div class="form-section-heading">
                <div><span class="eyebrow">WHEN</span><h3>Schedule</h3></div>
              </div>
              <p v-if="!scheduleEditable" class="form-warning">
                This {{ detail?.schedule.kind }} schedule cannot be safely reconstructed by the standard editor.
                Its schedule remains unchanged.
              </p>
              <div class="form-grid">
                <label class="form-field">
                  <span>Type</span>
                  <select v-model="form.scheduleKind" :disabled="!scheduleEditable">
                    <option value="cron">Cron expression</option>
                    <option value="every">Interval</option>
                    <option value="at">One-time</option>
                  </select>
                </label>
                <template v-if="form.scheduleKind === 'cron'">
                  <label class="form-field">
                    <span>Expression</span>
                    <input v-model="form.cronExpression" :disabled="!scheduleEditable" autocomplete="off" placeholder="0 7 * * *" required />
                  </label>
                  <label class="form-field">
                    <span>Timezone</span>
                    <input v-model="form.timeZone" :disabled="!scheduleEditable" autocomplete="off" placeholder="Europe/Berlin" />
                  </label>
                </template>
                <label v-else-if="form.scheduleKind === 'every'" class="form-field">
                  <span>Every (minutes)</span>
                  <input v-model.number="form.intervalMinutes" :disabled="!scheduleEditable" type="number" min="1" step="1" required />
                </label>
                <label v-else class="form-field">
                  <span>Run at</span>
                  <input v-model="form.runAt" :disabled="!scheduleEditable" type="datetime-local" required />
                </label>
                <label class="form-field">
                  <span>Wake mode</span>
                  <select v-model="form.wakeMode">
                    <option value="now">Now</option>
                    <option value="next-heartbeat">Next heartbeat</option>
                  </select>
                </label>
              </div>
            </section>

            <section class="form-section">
              <div class="form-section-heading">
                <div><span class="eyebrow">WHAT</span><h3>Agent-safe payload</h3></div>
              </div>
              <p v-if="!payloadEditable" class="form-warning">
                This payload has opaque input or environment configuration. Nexus leaves it untouched to avoid
                dropping secrets or unsupported fields.
              </p>
              <div class="form-grid">
                <label class="form-field">
                  <span>Payload type</span>
                  <select v-model="form.payloadKind" :disabled="!payloadEditable">
                    <option value="agentTurn">Agent turn</option>
                    <option value="systemEvent">System event</option>
                  </select>
                </label>
                <label class="form-field">
                  <span>Session target</span>
                  <input
                    v-model="form.sessionTarget"
                    autocomplete="off"
                    placeholder="isolated"
                    aria-describedby="session-target-help"
                  />
                  <small id="session-target-help">main, isolated, current or session:&lt;id&gt;</small>
                </label>
                <label class="form-field">
                  <span>Agent ID <small>optional</small></span>
                  <input v-model="form.agentId" autocomplete="off" />
                </label>
                <label class="form-field">
                  <span>Session key <small>optional</small></span>
                  <input v-model="form.sessionKey" autocomplete="off" />
                </label>
                <label class="form-field form-field--wide">
                  <span>{{ form.payloadKind === 'agentTurn' ? 'Agent message' : 'Event text' }}</span>
                  <textarea v-model="form.prompt" :disabled="!payloadEditable" rows="5" maxlength="50000" required></textarea>
                </label>
                <label v-if="form.payloadKind === 'agentTurn'" class="form-field">
                  <span>Model override <small>optional</small></span>
                  <input v-model="form.model" :disabled="!payloadEditable" autocomplete="off" />
                </label>
                <label class="checkbox-field form-field--wide">
                  <input v-model="form.deleteAfterRun" type="checkbox" />
                  Delete after a successful one-time run
                </label>
              </div>
            </section>

            <section class="form-section">
              <div class="form-section-heading">
                <div><span class="eyebrow">DELIVERY</span><h3>Result announcement</h3></div>
              </div>
              <p v-if="!deliveryEditable" class="form-warning">
                Completion or failure destinations are configured. The standard editor leaves delivery unchanged.
              </p>
              <div class="form-grid">
                <label class="form-field">
                  <span>Mode</span>
                  <select v-model="form.deliveryMode" :disabled="!deliveryEditable">
                    <option value="none">None</option>
                    <option value="announce">Announce</option>
                  </select>
                </label>
                <template v-if="form.deliveryMode === 'announce'">
                  <label class="form-field">
                    <span>Channel</span>
                    <input v-model="form.deliveryChannel" :disabled="!deliveryEditable" autocomplete="off" placeholder="telegram" />
                  </label>
                  <label class="form-field form-field--wide">
                    <span>Target <small>visible only in this owner editor</small></span>
                    <input v-model="form.deliveryTarget" :disabled="!deliveryEditable" autocomplete="off" />
                  </label>
                  <label class="checkbox-field form-field--wide">
                    <input v-model="form.bestEffort" :disabled="!deliveryEditable" type="checkbox" />
                    Best-effort delivery
                  </label>
                </template>
              </div>
            </section>

            <p class="safety-note">
              Command payloads and on-exit schedules are intentionally unavailable in this editor.
              Existing advanced definitions remain in OpenClaw unless an exposed field is changed.
            </p>
            <p v-if="localError || cronMutationError" class="inline-error" role="alert">
              {{ localError || cronMutationError }}
            </p>
            <div class="dialog-actions">
              <button type="button" class="nexus-button" :disabled="cronMutationLoading" @click="dialogMode = detail ? 'detail' : 'closed'">
                Cancel
              </button>
              <button type="submit" class="nexus-button nexus-button--primary" :disabled="cronMutationLoading">
                <Loader2 v-if="cronMutationLoading" :size="14" class="spin" aria-hidden="true" />
                <Save v-else :size="14" aria-hidden="true" />
                {{ dialogMode === 'create' ? 'Create schedule' : 'Save with hash guard' }}
              </button>
            </div>
          </form>

          <div v-else-if="dialogMode === 'delete'" class="dialog-body confirmation-body">
            <div class="confirmation-icon danger"><Trash2 :size="22" aria-hidden="true" /></div>
            <p>
              This removes <strong>{{ selectedName }}</strong> from OpenClaw. Nexus will send the currently
              loaded resource hash so a concurrent change is rejected instead of overwritten.
            </p>
            <p v-if="detail" class="hash-evidence"><span>Expected hash</span><code>{{ detail.resourceHash }}</code></p>
            <p v-if="localError || cronMutationError" class="inline-error" role="alert">
              {{ localError || cronMutationError }}
            </p>
            <div class="dialog-actions">
              <button type="button" class="nexus-button" :disabled="cronMutationLoading" @click="dialogMode = 'detail'">
                Cancel
              </button>
              <button
                type="button"
                class="nexus-button nexus-button--danger"
                data-autofocus
                :disabled="cronMutationLoading"
                @click="deleteJob"
              >
                <Loader2 v-if="cronMutationLoading" :size="14" class="spin" aria-hidden="true" />
                <Trash2 v-else :size="14" aria-hidden="true" />
                Delete schedule
              </button>
            </div>
          </div>

          <div v-else class="dialog-body confirmation-body">
            <div class="confirmation-icon"><Play :size="22" aria-hidden="true" /></div>
            <p>
              Nexus will ask OpenClaw to enqueue <strong>{{ selectedName }}</strong> in force mode.
              Queued is not the same as succeeded; the result will be tracked through <code>cron.runs</code>.
            </p>
            <p v-if="detail" class="hash-evidence"><span>Expected hash</span><code>{{ detail.resourceHash }}</code></p>
            <p v-if="localError || cronMutationError" class="inline-error" role="alert">
              {{ localError || cronMutationError }}
            </p>
            <div class="dialog-actions">
              <button type="button" class="nexus-button" :disabled="cronMutationLoading" @click="dialogMode = detail ? 'detail' : 'closed'">
                Cancel
              </button>
              <button
                type="button"
                class="nexus-button nexus-button--primary"
                data-autofocus
                :disabled="cronMutationLoading"
                @click="runJob"
              >
                <Loader2 v-if="cronMutationLoading" :size="14" class="spin" aria-hidden="true" />
                <Play v-else :size="14" aria-hidden="true" />
                Queue run
              </button>
            </div>
          </div>

          <footer v-if="dialogMode === 'detail' && detail" class="detail-actions">
            <p v-if="localError" class="inline-error" role="alert">{{ localError }}</p>
            <div>
              <button
                type="button"
                class="nexus-button"
                :disabled="!detail.canUpdate || cronMutationLoading"
                :title="detail.canUpdate ? undefined : capability('cron-update')?.reason || 'Update unavailable.'"
                @click="toggleEnabled"
              >
                <PowerOff v-if="detail.enabled" :size="14" aria-hidden="true" />
                <Power v-else :size="14" aria-hidden="true" />
                {{ detail.enabled ? 'Disable' : 'Enable' }}
              </button>
              <button
                type="button"
                class="nexus-button"
                :disabled="!detail.canUpdate || cronMutationLoading"
                @click="openEdit"
              >
                <Pencil :size="14" aria-hidden="true" />
                Edit
              </button>
              <button
                type="button"
                class="nexus-button nexus-button--danger"
                :disabled="!detail.canDelete || cronMutationLoading"
                @click="openDeleteConfirmation"
              >
                <Trash2 :size="14" aria-hidden="true" />
                Delete
              </button>
            </div>
            <button
              type="button"
              class="nexus-button nexus-button--primary"
              :disabled="!detail.canRun || cronMutationLoading"
              :title="detail.canRun ? undefined : capability('cron-run')?.reason || 'Manual run unavailable.'"
              @click="openRunConfirmation"
            >
              <Play :size="14" aria-hidden="true" />
              Run now
            </button>
          </footer>
        </section>
      </div>
    </Teleport>
  </div>
</template>

<style scoped>
.calendar-page { padding-bottom: 30px; }
.header-actions, .list-controls, .job-actions, .history-toolbar > div:last-child, .detail-actions > div {
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
}
.calendar-layout { display: grid; grid-template-columns: minmax(270px, .62fr) minmax(0, 1.65fr); gap: 14px; align-items: start; }
.calendar-panel { padding: 16px; }
.panel-heading { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding-bottom: 13px; border-bottom: 1px solid var(--line); }
.panel-heading h2, .history-toolbar h3, .form-section-heading h3 { margin: 3px 0 0; font-size: 16px; }
.jobs-heading { align-items: flex-end; }

.queued-banner { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 13px; border-color: var(--status-queue-line); background: linear-gradient(135deg, var(--status-queue-bg), var(--glass)); }
.queued-banner h2 { margin: 3px 0 2px; font-size: 16px; }
.queued-banner p { margin: 0; color: var(--tx-2); }
.queued-banner code { color: var(--st-queue); font-family: var(--font-mono-v2); font-size: 11px; }
.queued-icon, .upcoming-icon, .confirmation-icon { display: grid; place-items: center; border-radius: var(--r-sm); background: var(--status-queue-bg); color: var(--st-queue); }
.queued-icon { width: 40px; height: 40px; }

.upcoming-list { display: grid; gap: 2px; padding-top: 8px; }
.upcoming-item { width: 100%; display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 11px 7px; border: 0; border-bottom: 1px solid var(--line); background: transparent; color: inherit; text-align: left; cursor: pointer; }
.upcoming-item:last-child { border-bottom: 0; }
.upcoming-item:hover { background: var(--accent-wash); }
.upcoming-icon { width: 34px; height: 34px; background: var(--grad-soft); color: var(--a-mid); }
.upcoming-copy { display: grid; gap: 3px; min-width: 0; }
.upcoming-copy strong { color: var(--tx); }
.upcoming-copy time { color: var(--tx-2); }
.upcoming-copy code { color: var(--tx-3); font-family: var(--font-mono-v2); font-size: 11px; overflow-wrap: anywhere; }

.filter-toggle { min-height: 32px; display: inline-flex; align-items: center; gap: 7px; padding: 0 9px; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--tx-3); cursor: pointer; }
.filter-toggle[aria-pressed='true'] { border-color: var(--line-3); background: var(--accent-wash); color: var(--tx-2); }
.filter-indicator { width: 7px; height: 7px; border-radius: 50%; background: var(--tx-3); }
.filter-toggle[aria-pressed='true'] .filter-indicator { background: var(--st-work); box-shadow: var(--glow-work); }

.job-list { display: grid; gap: 8px; padding-top: 12px; }
.job-row { display: grid; grid-template-columns: 36px minmax(180px, 1fr) minmax(190px, auto) auto; align-items: center; gap: 12px; padding: 13px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--accent-wash); }
.job-state, .run-status { display: grid; place-items: center; border-radius: var(--r-sm); background: var(--status-queue-bg); color: var(--st-queue); }
.job-state { width: 36px; height: 36px; }
.job-state.success, .run-status.success { background: var(--status-work-bg); color: var(--st-work); }
.job-state.info, .run-status.info { background: var(--status-think-bg); color: var(--st-think); }
.job-state.danger, .run-status.danger { background: var(--status-block-bg); color: var(--st-block); }
.job-state.muted { background: var(--status-idle-bg); color: var(--tx-3); }
.job-copy { display: grid; gap: 5px; }
.job-title-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.job-title-row strong { color: var(--tx); }
.job-title-row .nexus-status { font-size: 11px; }
.job-copy p { margin: 0; color: var(--tx-2); line-height: 1.45; }
.job-identifiers { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; color: var(--tx-3); font-family: var(--font-mono-v2); font-size: 11px; }
.job-identifiers code { font: inherit; overflow-wrap: anywhere; }
.entity-link { color: var(--a-cyan); text-decoration: none; }
.entity-link:hover { color: var(--tx); text-decoration: underline; }
.entity-link:focus-visible { outline: 2px solid var(--a-mid); outline-offset: 3px; border-radius: 3px; }
.job-timing { display: grid; justify-items: end; gap: 4px; color: var(--tx-3); font-size: 11px; }
.job-timing span { display: flex; gap: 5px; }
.job-timing strong { color: var(--tx-2); font-weight: 600; }
.job-timing .job-error { max-width: 240px; display: block; color: var(--st-block); text-align: right; overflow-wrap: anywhere; }
.job-actions { justify-content: flex-end; flex-wrap: nowrap; }
.icon-button, .dialog-close { width: 36px; height: 36px; display: grid; place-items: center; border: 1px solid var(--line-2); border-radius: var(--r-sm); background: var(--accent-wash); color: var(--tx-2); cursor: pointer; }
.icon-button:hover:not(:disabled), .dialog-close:hover:not(:disabled) { border-color: var(--line-3); color: var(--tx); }
.icon-button:disabled { opacity: .45; cursor: not-allowed; }
.load-more { justify-self: center; margin-top: 4px; }

.compact-empty { display: grid; place-items: center; gap: 8px; min-height: 150px; color: var(--tx-3); text-align: center; }
.compact-empty p { margin: 0; }
.capability-note { grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; border-color: var(--status-queue-line); }
.capability-note h2 { font-size: 15px; }
.inline-error { margin: 12px 0 0; padding: 10px 12px; border: 1px solid var(--status-block-line); border-radius: var(--r-sm); background: var(--status-block-bg); color: var(--st-block); line-height: 1.45; overflow-wrap: anywhere; }

.cron-dialog-overlay { position: fixed; inset: 0; z-index: 400; display: grid; place-items: center; padding: 20px; overflow-y: auto; background: color-mix(in srgb, var(--space-0) 80%, transparent); backdrop-filter: blur(12px); }
.cron-dialog { width: min(100%, 980px); max-height: calc(100vh - 40px); display: grid; grid-template-rows: auto minmax(0, 1fr) auto; border: 1px solid var(--line-3); border-radius: var(--r-lg); background: color-mix(in srgb, var(--space-2) 96%, transparent); box-shadow: var(--panel-shadow); overflow: hidden; }
.cron-dialog--compact { width: min(100%, 560px); grid-template-rows: auto minmax(0, 1fr); }
.dialog-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 18px 20px; border-bottom: 1px solid var(--line); }
.dialog-header h2 { margin: 3px 0 0; font-family: var(--font-display); font-size: 21px; overflow-wrap: anywhere; }
.dialog-body { min-height: 0; padding: 18px 20px; overflow-x: hidden; overflow-y: auto; }
.detail-body { display: grid; align-content: start; gap: 15px; }
.detail-tabs { display: flex; gap: 4px; padding: 4px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--field-surface); width: fit-content; max-width: 100%; }
.detail-tabs button { min-height: 32px; padding: 0 12px; border: 0; border-radius: 7px; background: transparent; color: var(--tx-3); cursor: pointer; }
.detail-tabs button.active { background: var(--accent-wash-strong); color: var(--tx); }
.dialog-state { min-height: 230px; display: grid; place-items: center; align-content: center; gap: 9px; color: var(--tx-2); text-align: center; }
.dialog-state.error { color: var(--st-block); }
.dialog-state.empty { border: 1px dashed var(--line-2); border-radius: var(--r); }
.dialog-state p { margin: 0; }
.detail-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.detail-summary > div { display: grid; align-content: start; gap: 6px; padding: 12px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--accent-wash); }
.detail-summary strong { color: var(--tx); }
.detail-summary code, .hash-evidence code { color: var(--tx-2); font-family: var(--font-mono-v2); font-size: 11px; overflow-wrap: anywhere; }
.detail-description { margin: 0; color: var(--tx-2); line-height: 1.55; }
.definition-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.definition-card { padding: 14px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--accent-wash); }
.definition-card h3 { margin: 4px 0 10px; font-size: 16px; }
.definition-card > p { margin: 0; color: var(--tx-2); line-height: 1.5; overflow-wrap: anywhere; }
.definition-card dl { display: grid; gap: 0; margin: 9px 0 0; }
.definition-card dl div { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 8px; padding: 7px 0; border-top: 1px solid var(--line); }
.definition-card dt, .run-meta dt { color: var(--tx-3); font-family: var(--font-mono-v2); font-size: 10px; text-transform: uppercase; }
.definition-card dd, .run-meta dd { margin: 0; color: var(--tx-2); overflow-wrap: anywhere; }
.definition-card code { font-family: var(--font-mono-v2); font-size: 11px; }
.sensitive-value { color: var(--st-queue) !important; }
.detail-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 13px 20px; border-top: 1px solid var(--line); }
.detail-actions > .inline-error { flex: 1 1 100%; margin: 0; }

.tracked-run { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 10px; padding: 12px; border: 1px solid var(--status-queue-line); border-radius: var(--r-sm); background: var(--status-queue-bg); color: var(--st-queue); }
.tracked-run p { margin: 3px 0 0; color: var(--tx-2); }
.tracked-run code { font-family: var(--font-mono-v2); font-size: 11px; }
.history-toolbar { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; }
.contract-note {
  margin: 12px 0;
  padding: 9px 11px;
  color: var(--tx-2);
  font-size: 12px;
  line-height: 1.5;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: color-mix(in srgb, var(--space-2) 58%, transparent);
}
.contract-note strong { color: var(--tx); }
.contract-note code { color: var(--a-cyan); }
.run-list { display: grid; gap: 8px; }
.run-row { display: grid; grid-template-columns: 34px minmax(0, 1fr) minmax(170px, auto); gap: 11px; padding: 12px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--accent-wash); }
.run-row.tracked { border-color: var(--status-queue-line); background: linear-gradient(135deg, var(--status-queue-bg), var(--accent-wash)); }
.run-status { width: 34px; height: 34px; }
.run-copy { min-width: 0; }
.run-copy > div { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.run-copy code { color: var(--tx-3); font-family: var(--font-mono-v2); font-size: 11px; overflow-wrap: anywhere; }
.run-copy p { margin: 5px 0 0; color: var(--tx-2); line-height: 1.45; overflow-wrap: anywhere; }
.run-copy .run-error { color: var(--st-block); }
.run-copy details { margin-top: 8px; color: var(--tx-3); }
.run-copy summary { cursor: pointer; }
.run-copy ul { margin: 7px 0 0; padding-left: 18px; color: var(--tx-2); }
.run-meta { display: grid; align-content: start; gap: 5px; margin: 0; }
.run-meta div { display: grid; grid-template-columns: 65px minmax(0, 1fr); gap: 7px; }

.cron-form { display: grid; gap: 14px; }
.form-section { display: grid; gap: 12px; padding: 15px; border: 1px solid var(--line); border-radius: var(--r); background: var(--accent-wash); }
.form-section-heading { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 11px; }
.form-field { display: grid; align-content: start; gap: 6px; color: var(--tx-2); }
.form-field--wide { grid-column: 1 / -1; }
.form-field > span { color: var(--tx-2); font-size: 12px; font-weight: 650; }
.form-field small { color: var(--tx-3); font-weight: 500; }
.form-field :is(input, select, textarea) { width: 100%; min-width: 0; min-height: 38px; padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--field-surface); color: var(--tx); font: inherit; box-sizing: border-box; }
.form-field textarea { resize: vertical; line-height: 1.5; }
.form-field :is(input, select, textarea):focus { border-color: var(--a-blue); outline: none; box-shadow: var(--focus-ring); }
.form-field :is(input, select, textarea):disabled { opacity: .58; cursor: not-allowed; }
.checkbox-field { display: inline-flex; align-items: center; gap: 8px; color: var(--tx-2); cursor: pointer; }
.checkbox-field input { width: 16px; height: 16px; accent-color: var(--a-mid); }
.form-warning, .safety-note { margin: 0; padding: 10px 12px; border: 1px solid var(--status-queue-line); border-radius: var(--r-sm); background: var(--status-queue-bg); color: var(--tx-2); line-height: 1.5; }
.safety-note { font-size: 12px; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 9px; flex-wrap: wrap; }

.confirmation-body { display: grid; justify-items: center; gap: 15px; padding: 28px 24px 22px; text-align: center; }
.confirmation-body > p { margin: 0; color: var(--tx-2); line-height: 1.6; }
.confirmation-icon { width: 48px; height: 48px; background: var(--grad-soft); color: var(--a-mid); }
.confirmation-icon.danger { background: var(--status-block-bg); color: var(--st-block); }
.hash-evidence { width: 100%; display: grid; gap: 5px; padding: 10px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--field-surface); }
.hash-evidence span { color: var(--tx-3); font-size: 11px; text-transform: uppercase; }
.confirmation-body .dialog-actions { width: 100%; }

.spin { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

@media (max-width: 1120px) {
  .calendar-layout { grid-template-columns: 1fr; }
  .detail-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 820px) {
  .job-row { grid-template-columns: 36px minmax(0, 1fr) auto; }
  .job-timing { grid-column: 2 / -1; justify-items: start; }
  .job-timing .job-error { max-width: none; text-align: left; }
  .definition-grid { grid-template-columns: 1fr; }
  .run-row { grid-template-columns: 34px minmax(0, 1fr); }
  .run-meta { grid-column: 2; }
}

@media (max-width: 620px) {
  .header-actions { width: 100%; }
  .header-actions .nexus-button { flex: 1; }
  .queued-banner, .capability-note { grid-template-columns: auto minmax(0, 1fr); }
  .queued-banner > .nexus-button, .capability-note > .nexus-button { grid-column: 1 / -1; justify-self: stretch; }
  .jobs-heading, .history-toolbar, .detail-actions { align-items: stretch; flex-direction: column; }
  .list-controls { justify-content: space-between; }
  .job-row { grid-template-columns: 36px minmax(0, 1fr); }
  .job-actions, .job-timing { grid-column: 1 / -1; }
  .job-actions .nexus-button { flex: 1; }
  .detail-summary, .form-grid { grid-template-columns: 1fr; }
  .form-field--wide { grid-column: auto; }
  .cron-dialog-overlay { padding: 10px; place-items: start center; }
  .cron-dialog { max-height: calc(100vh - 20px); border-radius: var(--r); }
  .dialog-header, .dialog-body, .detail-actions { padding-left: 14px; padding-right: 14px; }
  .detail-actions > div, .detail-actions > .nexus-button { width: 100%; }
  .detail-actions .nexus-button { flex: 1; }
  .run-row { grid-template-columns: 30px minmax(0, 1fr); }
  .run-status { width: 30px; height: 30px; }
  .run-meta { grid-column: 1 / -1; }
}

@media (max-width: 390px) {
  .job-state { display: none; }
  .job-row { grid-template-columns: minmax(0, 1fr); }
  .job-copy, .job-actions, .job-timing { grid-column: 1; }
  .dialog-actions .nexus-button { width: 100%; }
  .detail-tabs { width: 100%; }
  .detail-tabs button { flex: 1; }
}
</style>
