{% extends 'base.html' %}{% block content %}
<div class="toolbar"><h1>{{ t('jobs.title', 'Job {id}').replace('{id}', job.id|string) }}</h1><div class="toolbar-actions">{% if can_restart %}<form method="post" action="/jobs/{{ job.id }}/restart" class="inline-form" onsubmit="return confirm({{ t('jobs.restart.confirm')|tojson }});"><button type="submit">↻ {{ t('jobs.restart') }}</button></form>{% endif %}<a class="button button-secondary" href="/software">{{ t('common.back') }}</a></div></div>
<section class="panel"><dl><dt>{{ t('software.job_id') }}</dt><dd><code>{{ job.id }}</code></dd><dt>{{ t('common.assets') }}</dt><dd><a href="/assets/{{ job.asset.id }}?inventory_tab=software">{{ job.asset.name }}</a></dd><dt>{{ t('jobs.type') }}</dt><dd>{{ t(job.display_job_type_key, job.display_job_type_fallback) }}</dd><dt>{{ t('software.package') }}</dt><dd>{{ job.display_package_name }}</dd><dt>{{ t('jobs.action') }}</dt><dd>{{ t('jobs.action.' ~ job.action, job.action) }}</dd><dt>{{ t('jobs.priority') }}</dt><dd>{{ job.priority }}</dd><dt>{{ t('software.status') }}</dt><dd><span id="job-status" class="job-status job-status-{{ job.status }}">{{ t('software.status.' ~ job.status, job.status) }}</span></dd><dt>{{ t('software.platform') }}</dt><dd>{{ job.platform }}</dd><dt>{{ t('software.created') }}</dt><dd>{% if job.created_at %}<time class="local-time" datetime="{{ utc_iso(job.created_at) }}" data-utc="{{ utc_iso(job.created_at) }}">{{ job.created_at }}</time>{% else %}{{ '' }}{% endif %}</dd><dt>{{ t('jobs.created_by') }}</dt><dd>{{ job.created_by or '—' }}</dd><dt>{{ t('jobs.scheduled_at') }}</dt><dd>{% if job.scheduled_at %}<time class="local-time" datetime="{{ utc_iso(job.scheduled_at) }}" data-utc="{{ utc_iso(job.scheduled_at) }}">{{ job.scheduled_at }}</time>{% else %}{{ '—' }}{% endif %}</dd><dt>{{ t('jobs.attempts') }}</dt><dd>{{ job.attempt_count }} / {{ job.max_attempts }}</dd><dt>{{ t('software.sent') }}</dt><dd>{% if job.sent_at %}<time class="local-time" datetime="{{ utc_iso(job.sent_at) }}" data-utc="{{ utc_iso(job.sent_at) }}">{{ job.sent_at }}</time>{% else %}{{ '' }}{% endif %}</dd><dt>{{ t('software.finished') }}</dt><dd id="job-finished">{% if job.finished_at %}<time class="local-time" datetime="{{ utc_iso(job.finished_at) }}" data-utc="{{ utc_iso(job.finished_at) }}">{{ job.finished_at }}</time>{% else %}{{ '' }}{% endif %}</dd><dt>{{ t('software.exit_code') }}</dt><dd id="job-exit-code">{{ job.exit_code if job.exit_code is not none else '' }}</dd><dt>{{ t('software.message') }}</dt><dd id="job-message">{{ job.message or '' }}</dd></dl></section>
<section class="panel job-data-grid"><div><h2>{{ t('jobs.parameters') }}</h2><pre class="sync-log">{{ job.parameters | tojson(indent=2) }}</pre></div><div><h2>{{ t('jobs.result') }}</h2><pre class="sync-log">{{ job.result_data | tojson(indent=2) }}</pre></div></section>
<section class="panel"><h2>{{ t('jobs.history') }}</h2><div class="table-scroll"><table class="data-table"><thead><tr><th>{{ t('software.time') }}</th><th>{{ t('jobs.attempt') }}</th><th>{{ t('jobs.event') }}</th><th>{{ t('software.status') }}</th><th>{{ t('software.message') }}</th><th>{{ t('jobs.created_by') }}</th></tr></thead><tbody>{% for event in events %}<tr><td><time class="local-time" datetime="{{ utc_iso(event.created_at) }}" data-utc="{{ utc_iso(event.created_at) }}">{{ event.created_at }}</time></td><td>{{ event.attempt }}</td><td>{{ t('jobs.event.' ~ event.event_type, event.event_type) }}</td><td>{{ t('software.status.' ~ event.status, event.status or '—') }}</td><td>{{ event.message or '' }}</td><td>{{ event.created_by or '—' }}</td></tr>{% else %}<tr><td colspan="6">{{ t('jobs.history.empty') }}</td></tr>{% endfor %}</tbody></table></div></section>
<section class="panel"><h2>{{ t('software.diagnostics') }}</h2><p><strong>{{ t('software.local_log') }}:</strong> {% if job.platform == 'windows' %}<code>C:\ProgramData\AssetManager\commands.log</code>{% elif job.platform == 'linux' %}<code>/var/log/assetmanager-commands.log</code>{% elif job.platform == 'macos' %}<code>/Library/Logs/AssetManager-commands.log</code>{% else %}<code>—</code>{% endif %}</p><p><strong>{{ t('software.callback_health') }}:</strong> <code>/api/software-callback/health</code></p><p class="notice"><strong>{{ t('software.callback_token_warning') }}</strong></p>{% if job.resolved_script %}<label>{{ t('jobs.resolved_script') }}<textarea id="resolved-job-script" class="custom-css-editor" rows="24" readonly>{{ job.resolved_script }}</textarea><small>{{ t('jobs.resolved_script_help') }}</small></label><button type="button" class="button button-secondary" id="copy-resolved-script">{{ t('jobs.copy_resolved_script') }}</button>{% endif %}<details style="margin-top:1rem"><summary>{{ t('jobs.meshctrl_command') }}</summary><label>{{ t('software.manual_command') }}<textarea id="manual-job-command" class="custom-css-editor" readonly>{{ job.command_preview or '' }}</textarea></label><button type="button" class="button button-secondary" id="copy-job-command">{{ t('software.copy_command') }}</button></details></section>
<section class="panel"><div class="software-log-heading"><h2>{{ t('software.client_log') }}</h2><button type="button" class="button button-secondary" data-copy-log="job-client-log">{{ t('software.copy_log') }}</button></div><pre class="sync-log" id="job-client-log" data-localize-log>{{ job.log_text or t('software.no_callback') }}</pre></section><section class="panel"><h2>{{ t('software.meshctrl') }}</h2><div class="software-log-heading"><h3>{{ t('software.stdout') }}</h3><button type="button" class="button button-secondary" data-copy-log="job-meshctrl-stdout">{{ t('software.copy_log') }}</button></div><pre class="sync-log" id="job-meshctrl-stdout" data-localize-log>{{ job.meshctrl_stdout or '' }}</pre><div class="software-log-heading"><h3>{{ t('software.stderr') }}</h3><button type="button" class="button button-secondary" data-copy-log="job-meshctrl-stderr">{{ t('software.copy_log') }}</button></div><pre class="sync-log" id="job-meshctrl-stderr" data-localize-log>{{ job.meshctrl_stderr or '' }}</pre></section>
<script>
async function copyTextRobust(text) {
  const value = String(text || '');
  if (!value) return false;

  try {
    if (navigator.clipboard && window.isSecureContext) {
      await navigator.clipboard.writeText(value);
      return true;
    }
  } catch (_) {}

  const area = document.createElement('textarea');
  area.value = value;
  area.setAttribute('readonly', '');
  area.style.position = 'fixed';
  area.style.left = '-9999px';
  area.style.top = '0';
  area.style.opacity = '0';
  document.body.appendChild(area);
  area.focus();
  area.select();
  area.setSelectionRange(0, area.value.length);

  let copied = false;
  try {
    copied = document.execCommand('copy');
  } catch (_) {
    copied = false;
  }
  area.remove();
  return copied;
}
(()=>{document.getElementById('copy-resolved-script')?.addEventListener('click',async event=>{const ok=await copyTextRobust(document.getElementById('resolved-job-script')?.value||'');event.currentTarget.textContent=ok?{{ t('software.copied')|tojson }}:{{ t('software.copy_failed')|tojson }};setTimeout(()=>event.currentTarget.textContent={{ t('jobs.copy_resolved_script')|tojson }},1600);});document.getElementById('copy-job-command')?.addEventListener('click',async event=>{const ok=await copyTextRobust(document.getElementById('manual-job-command')?.value||'');event.currentTarget.textContent=ok?{{ t('software.copied')|tojson }}:{{ t('software.copy_failed')|tojson }};setTimeout(()=>event.currentTarget.textContent={{ t('software.copy_command')|tojson }},1600);});document.querySelectorAll('[data-copy-log]').forEach(button=>button.addEventListener('click',async event=>{const ok=await copyTextRobust(document.getElementById(button.dataset.copyLog)?.textContent||'');event.currentTarget.textContent=ok?{{ t('software.copied')|tojson }}:{{ t('software.copy_failed')|tojson }};setTimeout(()=>event.currentTarget.textContent={{ t('software.copy_log')|tojson }},1600);}));const terminal=new Set(['success','failed','partial','timeout','cancelled']);async function refreshJob(){try{const r=await fetch('/api/software-jobs/{{ job.id }}',{cache:'no-store'});if(!r.ok)return;const d=await r.json();const st=document.getElementById('job-status');if(st){st.textContent=d.status_label;st.className='job-status job-status-'+d.status;}document.getElementById('job-message').textContent=d.message||'';const finished=document.getElementById('job-finished');if(finished){finished.innerHTML=d.finished_at?`<time class="local-time" datetime="${d.finished_at}" data-utc="${d.finished_at}">${d.finished_at}</time>`:'';window.AssetManagerTime?.scan(finished);}document.getElementById('job-exit-code').textContent=d.exit_code??'';document.getElementById('job-client-log').textContent=d.log_text||{{ t('software.no_callback')|tojson }};if(!terminal.has(d.status))setTimeout(refreshJob,3000);}catch(_){setTimeout(refreshJob,5000);}}{% if job.status not in ['success','failed','partial','timeout','cancelled'] %}setTimeout(refreshJob,1500);{% endif %}})();</script>
{% endblock %}