// Dashboard extras - Jobs + Settings pages.
// Companion to dashboard.jsx; loaded after it. Reuses live dashboard API helpers.

// --- Jobs --------------------------------------------------------------------
const JobStatusPill = ({ status }) => {
  const map = {
    queued:     { bg:'#eef0f2', fg:'#3b424b', label:'queued'     },
    processing: { bg:'#fff3d6', fg:'#7a5b00', label:'processing' },
    completed:  { bg:'#e7f4ec', fg:'#1e6b3a', label:'completed'  },
    failed:     { bg:'#fceaea', fg:'#992b2b', label:'failed'     },
  };
  const s = map[status] || map.queued;
  return (
    <span style={{
      fontFamily: wf.mono, fontSize: 10.5, padding: '2px 7px', borderRadius: 999,
      background: s.bg, color: s.fg, textTransform: 'uppercase', letterSpacing: 0.6,
      whiteSpace: 'nowrap',
    }}>{s.label}</span>
  );
};

const DashJobs = () => {
  const [status, setStatus] = React.useState('all');
  const [query, setQuery] = React.useState('');
  const jobsState = useDashApi('/jobs');
  const jobs = (jobsState.data?.jobs || []).map(normalizeJob);
  const counts = {
    all: jobs.length,
    queued: jobs.filter(j=>j.status==='queued').length,
    processing: jobs.filter(j=>j.status==='processing').length,
    completed: jobs.filter(j=>jobOk(j.status)).length,
    failed: jobs.filter(j=>j.status==='failed').length,
  };
  const q = query.trim().toLowerCase();
  const visibleJobs = jobs.filter(j => {
    if (status === 'completed' && !jobOk(j.status)) return false;
    if (status !== 'all' && status !== 'completed' && j.status !== status) return false;
    if (!q) return true;
    return `${j.id} ${j.endpoint} ${j.media} ${j.status}`.toLowerCase().includes(q);
  });

  return (
    <DashChrome current="Jobs">
      <div style={{ marginBottom: 22, display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap' }}>
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <div style={{ fontFamily: wf.mono, fontSize: 11, letterSpacing: 1, color: 'var(--accent)', textTransform: 'uppercase' }}>jobs</div>
            <DataBadge live={!!jobsState.data} />
          </div>
          <h1 style={{ fontFamily: wf.sans, fontSize: 30, fontWeight: 600, letterSpacing: -0.4, margin: '6px 0 0' }}>Every job your team has run</h1>
          <p style={{ fontFamily: wf.sans, fontSize: 14, color: wf.ink3, margin: '6px 0 0' }}>Async jobs and inline calls land here. Retry failures, cancel queued work, jump to results.</p>
        </div>
      </div>
      {jobsState.error && <div style={{ ...dashStyles.card, borderColor: '#f0c4c4', color: '#992b2b', marginBottom: 14 }}>Could not load jobs: {jobsState.error.message}</div>}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14, flexWrap: 'wrap' }}>
        <Pill active={status === 'all'} onClick={() => setStatus('all')}>All - {counts.all}</Pill>
        <Pill active={status === 'queued'} onClick={() => { setStatus('queued'); dashTrack('jobs_filter_changed', { status: 'queued' }); }}>Queued - {counts.queued}</Pill>
        <Pill active={status === 'processing'} onClick={() => { setStatus('processing'); dashTrack('jobs_filter_changed', { status: 'processing' }); }}>Processing - {counts.processing}</Pill>
        <Pill active={status === 'completed'} onClick={() => { setStatus('completed'); dashTrack('jobs_filter_changed', { status: 'completed' }); }}>Completed - {counts.completed}</Pill>
        <Pill active={status === 'failed'} onClick={() => { setStatus('failed'); dashTrack('jobs_filter_changed', { status: 'failed' }); }}>Failed - {counts.failed}</Pill>
        <span style={{ flex: 1 }} />
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: '6px 10px', borderRadius: 7, border: `1px solid ${wf.line}`, background: wf.card, fontFamily: wf.sans, fontSize: 12.5, color: wf.ink3, minWidth: 220 }}>
          <svg width="11" height="11" viewBox="0 0 12 12"><circle cx="5" cy="5" r="3.2" fill="none" stroke="currentColor" strokeWidth="1.4"/><line x1="7.5" y1="7.5" x2="10" y2="10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/></svg>
          <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Filter by job, endpoint, media..." style={{ border: 0, outline: 0, background: 'transparent', flex: 1, fontFamily: wf.sans, fontSize: 12.5, color: wf.ink }} />
        </span>
      </div>
      <div style={{...dashStyles.card, padding: 0, overflow:'hidden'}}>
        <table style={{ width:'100%', borderCollapse:'collapse', fontFamily: wf.sans, fontSize: 13 }}>
          <thead><tr style={{ background: wf.cardAlt, color: wf.ink3, textAlign:'left', fontFamily: wf.mono, fontSize: 11, letterSpacing: 1, textTransform: 'uppercase' }}>
            <th style={{ padding: '10px 14px' }}>Job ID</th><th style={{ padding: '10px 14px' }}>Endpoint</th><th style={{ padding: '10px 14px' }}>Media</th><th style={{ padding: '10px 14px', textAlign:'right' }}>Duration</th><th style={{ padding: '10px 14px', textAlign:'right' }}>Est cost</th><th style={{ padding: '10px 14px', textAlign:'right' }}>Final cost</th><th style={{ padding: '10px 14px' }}>Status</th><th style={{ padding: '10px 14px' }}>Created</th><th style={{ padding: '10px 14px' }}></th>
          </tr></thead>
          <tbody>
            {visibleJobs.map((j) => (
              <React.Fragment key={j.id}>
                <tr style={{ borderTop: `1px solid ${wf.line}` }}>
                  <td style={{ padding: '12px 14px', fontFamily: wf.mono, fontSize: 11.5, color: wf.ink2 }}>{j.id}</td>
                  <td style={{ padding: '12px 14px', fontFamily: wf.mono, fontSize: 12 }}><a href={endpointDocHref(j.endpoint)} style={{ color: wf.ink, textDecoration: 'none' }}>{endpointPath(j.endpoint)}</a></td>
                  <td style={{ padding: '12px 14px', color: wf.ink3, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{mediaName(j.media)}</td>
                  <td style={{ padding: '12px 14px', fontFamily: wf.mono, fontSize: 12, color: wf.ink3, textAlign:'right' }}>{formatSeconds(j.duration)}</td>
                  <td style={{ padding: '12px 14px', fontFamily: wf.mono, fontSize: 12, color: wf.ink3, textAlign:'right' }}>{formatMoney(j.estimated)}</td>
                  <td style={{ padding: '12px 14px', fontFamily: wf.mono, fontSize: 12, textAlign:'right' }}>{j.final == null ? <span style={{ color: wf.ink4 }}>-</span> : formatMoney(j.final)}</td>
                  <td style={{ padding: '12px 14px' }}><JobStatusPill status={j.status} /></td>
                  <td style={{ padding: '12px 14px', fontFamily: wf.mono, fontSize: 11.5, color: wf.ink3 }}>{j.created || '-'}</td>
                  <td style={{ padding: '12px 14px', textAlign: 'right', whiteSpace: 'nowrap' }}>
                    {jobOk(j.status) && <span onClick={() => dashTrack('job_details_viewed', { job_id: j.id, endpoint_id: j.endpoint, status: j.status })} style={{ fontFamily: wf.mono, fontSize: 11, color: 'var(--accent)', cursor: 'pointer' }}>result</span>}
                    {j.status === 'queued' && <span onClick={() => dashTrack('job_cancel_clicked', { job_id: j.id, endpoint_id: j.endpoint })} style={{ fontFamily: wf.mono, fontSize: 11, color: '#c33', cursor: 'pointer' }}>cancel</span>}
                    {j.status === 'failed' && <span onClick={() => dashTrack('job_retry_clicked', { job_id: j.id, endpoint_id: j.endpoint, error_excerpt: (j.error || '').split(' - ')[0] })} style={{ fontFamily: wf.mono, fontSize: 11, color: 'var(--accent)', cursor: 'pointer' }}>retry</span>}
                    {j.status === 'processing' && <span style={{ fontFamily: wf.mono, fontSize: 11, color: wf.ink3 }}>-</span>}
                  </td>
                </tr>
                {j.error && <tr style={{ background: '#fdf6f6' }}><td colSpan={9} style={{ padding: '8px 14px 12px', fontFamily: wf.mono, fontSize: 11.5, color: '#992b2b' }}><span style={{ marginRight: 8, fontWeight: 600 }}>error:</span>{j.error}</td></tr>}
              </React.Fragment>
            ))}
            {!visibleJobs.length && <tr><td colSpan="9" style={{ padding: '18px 14px', color: wf.ink3 }}>{jobsState.loading ? 'Loading jobs...' : 'No jobs match that filter.'}</td></tr>}
          </tbody>
        </table>
      </div>
      <div style={{ marginTop: 14, fontFamily: wf.sans, fontSize: 12, color: wf.ink3 }}>Jobs older than 30 days can be archived after retention rules are configured.</div>
    </DashChrome>
  );
};

const SettingsRow = ({ title, hint, children }) => (
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1.3fr', gap: 24, padding: '16px 0', borderBottom: `1px solid ${wf.line}`, alignItems: 'start' }}>
    <div>
      <div style={{ fontFamily: wf.sans, fontSize: 14, fontWeight: 600, color: wf.ink }}>{title}</div>
      {hint && <div style={{ fontFamily: wf.sans, fontSize: 12.5, color: wf.ink3, lineHeight: 1.55, marginTop: 4 }}>{hint}</div>}
    </div>
    <div>{children}</div>
  </div>
);

const fieldStyle = (mono) => ({
  width: '100%', boxSizing: 'border-box', padding: '8px 12px', borderRadius: 7,
  border: `1px solid ${wf.line}`, background: wf.card, fontFamily: mono ? wf.mono : wf.sans,
  fontSize: 13, color: wf.ink,
});
const TextInput = ({ value, onChange, mono, width, type = 'text', disabled = false, min, max, step, placeholder }) => (
  <input type={type} value={value ?? ''} min={min} max={max} step={step} placeholder={placeholder} disabled={disabled} onChange={e => onChange && onChange(e.target.value)} style={{ ...fieldStyle(mono), width: width || '100%', opacity: disabled ? 0.68 : 1 }} />
);
const SelectInput = ({ value, onChange, options = [] }) => (
  <select value={value} onChange={e => onChange && onChange(e.target.value)} style={{ ...fieldStyle(false), minWidth: 220 }}>
    {options.map(option => <option key={option.value || option} value={option.value || option}>{option.label || option}</option>)}
  </select>
);
const ToggleInput = ({ checked, onChange, label }) => (
  <label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontFamily: wf.sans, fontSize: 13, color: wf.ink2, cursor: 'pointer' }}>
    <input type="checkbox" checked={!!checked} onChange={e => onChange && onChange(e.target.checked)} style={{ accentColor: 'var(--accent)' }} />
    {label}
  </label>
);

const DashSettings = () => {
  const settingsState = useDashApi('/settings');
  const session = dashSession() || {};
  const [form, setForm] = React.useState(null);
  const [saving, setSaving] = React.useState(false);
  const [dirty, setDirty] = React.useState(false);
  const [message, setMessage] = React.useState('');
  const [error, setError] = React.useState('');

  React.useEffect(() => {
    const settings = settingsState.data?.settings;
    if (!settings || form) return;
    setForm({
      organization: {
        name: settings.organization?.name || session.orgName || 'MomentIQ account',
        id: settings.organization?.id || session.orgId,
        primary_contact: settings.organization?.primary_contact || session.userEmail || '',
      },
      api_defaults: {
        default_quality: settings.api_defaults?.default_quality || 'std',
        default_result_expiration_hours: Number(settings.api_defaults?.default_result_expiration_hours || 24),
        webhook_url: settings.api_defaults?.webhook_url || '',
        notification_email: settings.api_defaults?.notification_email || session.userEmail || '',
      },
      spending: {
        monthly_spending_cap_usd: Number(settings.spending?.monthly_spending_cap_usd || 0),
        usage_alerts_enabled: !!settings.spending?.usage_alerts_enabled,
        usage_alerts: (settings.spending?.usage_alerts || []).length ? settings.spending.usage_alerts : [
          { threshold: 50, channel: 'email', target: settings.api_defaults?.notification_email || session.userEmail || '', enabled: true },
          { threshold: 80, channel: 'email', target: settings.api_defaults?.notification_email || session.userEmail || '', enabled: true },
          { threshold: 100, channel: 'email', target: settings.api_defaults?.notification_email || session.userEmail || '', enabled: true },
        ],
      },
    });
    setDirty(false);
  }, [settingsState.data]);

  const setGroup = (group, key, value) => {
    setDirty(true); setMessage('');
    setForm(prev => ({ ...prev, [group]: { ...prev[group], [key]: value } }));
  };
  const setAlert = (index, key, value) => {
    setDirty(true); setMessage('');
    setForm(prev => ({
      ...prev,
      spending: {
        ...prev.spending,
        usage_alerts: prev.spending.usage_alerts.map((alert, i) => i === index ? { ...alert, [key]: value } : alert),
      },
    }));
  };
  const addAlert = () => {
    setDirty(true); setMessage('');
    setForm(prev => ({
      ...prev,
      spending: { ...prev.spending, usage_alerts: [...prev.spending.usage_alerts, { threshold: 90, channel: 'email', target: prev.api_defaults.notification_email || session.userEmail || '', enabled: true }] },
    }));
  };
  const removeAlert = (index) => {
    setDirty(true); setMessage('');
    setForm(prev => ({
      ...prev,
      spending: { ...prev.spending, usage_alerts: prev.spending.usage_alerts.filter((_, i) => i !== index) },
    }));
  };

  const saveSettings = async () => {
    if (!form) return;
    setSaving(true); setError(''); setMessage('');
    try {
      const payload = {
        organization: form.organization,
        api_defaults: form.api_defaults,
        spending: {
          monthly_spending_cap_usd: Number(form.spending.monthly_spending_cap_usd || 0),
          usage_alerts_enabled: !!form.spending.usage_alerts_enabled,
          usage_alerts: form.spending.usage_alerts.map(a => ({
            threshold: Number(a.threshold || 0),
            channel: a.channel || 'email',
            target: a.target || '',
            enabled: a.enabled !== false,
          })),
        },
      };
      const saved = await dashApi('/settings', { method: 'PATCH', body: payload });
      setForm(saved.settings);
      setDirty(false);
      const nextSession = dashSession() || {};
      localStorage.setItem(DASH_SESSION_KEY, JSON.stringify({
        ...nextSession,
        orgName: saved.settings?.organization?.name || nextSession.orgName || 'MomentIQ account',
        orgId: saved.settings?.organization?.id || nextSession.orgId,
        userEmail: saved.settings?.organization?.primary_contact || nextSession.userEmail || '',
      }));
      setMessage('Settings saved. Spend cap enforcement updates immediately.');
      setTimeout(() => setMessage(''), 3000);
      dashTrack('settings_saved', { section: 'all', spending_cap_usd: payload.spending.monthly_spending_cap_usd, usage_alert_count: payload.spending.usage_alerts.length });
    } catch (err) {
      setError(err?.payload?.error?.message || err.message || 'Could not save settings.');
    } finally {
      setSaving(false);
    }
  };

  const org = form?.organization || {};
  const api = form?.api_defaults || {};
  const spending = form?.spending || {};
  const alerts = spending.usage_alerts || [];
  const cap = Number(spending.monthly_spending_cap_usd || 0);
  const saveButton = () => (
    <button type="button" onClick={saveSettings} disabled={!form || saving || !dirty} style={{ border: 0, background: 'transparent', padding: 0, cursor: (!form || saving || !dirty) ? 'default' : 'pointer', opacity: (!form || saving || !dirty) ? 0.58 : 1 }}>
      <Btn primary size={14}>{saving ? 'Saving...' : 'Save changes'}</Btn>
    </button>
  );
  return (
    <DashChrome current="Settings">
      <div style={{ marginBottom: 22, display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap' }}>
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <div style={{ fontFamily: wf.mono, fontSize: 11, letterSpacing: 1, color: 'var(--accent)', textTransform: 'uppercase' }}>settings</div>
            <DataBadge live={!!settingsState.data} />
          </div>
          <h1 style={{ fontFamily: wf.sans, fontSize: 30, fontWeight: 600, letterSpacing: -0.4, margin: '6px 0 0' }}>Org &amp; account settings</h1>
          <p style={{ fontFamily: wf.sans, fontSize: 14, color: wf.ink3, margin: '6px 0 0' }}>Edit defaults, alerts, and the hard monthly spend cap for this account.</p>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
          {dirty && <span style={{ fontFamily: wf.mono, fontSize: 11, color: '#9a6300', background: '#fff6dd', border: '1px solid #ead49a', borderRadius: 999, padding: '4px 8px', textTransform: 'uppercase' }}>unsaved changes</span>}
          {saveButton()}
        </div>
      </div>
      {settingsState.error && <div style={{ ...dashStyles.card, borderColor: '#f0c4c4', color: '#992b2b', marginBottom: 14 }}>Could not load settings: {settingsState.error.message}</div>}
      {error && <div style={{ ...dashStyles.card, borderColor: '#f0c4c4', color: '#992b2b', marginBottom: 14 }}>{error}</div>}
      {message && <div style={{ ...dashStyles.card, borderColor: '#b8dfc6', color: '#1e6b3a', marginBottom: 14 }}>{message}</div>}
      {!form && <div style={dashStyles.card}>Loading settings...</div>}
      {form && <>
      <div style={dashStyles.card}>
        <div style={{ fontFamily: wf.sans, fontSize: 16, fontWeight: 600, marginBottom: 4 }}>Organization</div>
        <div style={{ fontFamily: wf.sans, fontSize: 12.5, color: wf.ink3, marginBottom: 6 }}>The team identity that owns this MomentIQ account.</div>
        <SettingsRow title="Organization name" hint="Shown on invoices and in audit logs."><TextInput value={org.name} onChange={v=>setGroup('organization','name',v)} /></SettingsRow>
        <SettingsRow title="Organization ID" hint="Stable identifier. Used in support and webhooks."><TextInput value={org.id} mono disabled /></SettingsRow>
        <SettingsRow title="Primary contact" hint="Reachable for billing and account issues."><TextInput value={org.primary_contact} onChange={v=>setGroup('organization','primary_contact',v)} /></SettingsRow>
      </div>
      <div style={{...dashStyles.card, marginTop: 14}}>
        <div style={{ fontFamily: wf.sans, fontSize: 16, fontWeight: 600, marginBottom: 4 }}>API defaults</div>
        <div style={{ fontFamily: wf.sans, fontSize: 12.5, color: wf.ink3, marginBottom: 6 }}>Applied to API calls when the request does not override the field.</div>
        <SettingsRow title="Default processing quality" hint='Tradeoff between latency and accuracy. Per-call "quality" overrides this.'><SelectInput value={api.default_quality} onChange={v=>setGroup('api_defaults','default_quality',v)} options={['low','std','high']} /></SettingsRow>
        <SettingsRow title="Default result expiration" hint="How long signed URLs and stored results stay live."><TextInput type="number" min="1" max="168" step="1" value={api.default_result_expiration_hours} onChange={v=>setGroup('api_defaults','default_result_expiration_hours',Number(v))} /></SettingsRow>
        <SettingsRow title="Webhook URL" hint="POSTed when async jobs complete or fail. Signed webhook delivery still needs final implementation."><TextInput value={api.webhook_url} onChange={v=>setGroup('api_defaults','webhook_url',v)} mono /></SettingsRow>
        <SettingsRow title="Notification email" hint="For job failures, cap alerts, and product updates."><TextInput value={api.notification_email} onChange={v=>setGroup('api_defaults','notification_email',v)} /></SettingsRow>
      </div>
      <div style={{...dashStyles.card, marginTop: 14}}>
        <div style={{ fontFamily: wf.sans, fontSize: 16, fontWeight: 600, marginBottom: 4 }}>Spending controls</div>
        <div style={{ fontFamily: wf.sans, fontSize: 12.5, color: wf.ink3, marginBottom: 6 }}>A spend cap is a hard monthly dollar limit. Calls that would pass it return <code style={{ fontFamily: wf.mono }}>402 cap_exceeded</code>. Set 0 for no cap.</div>
        <SettingsRow title="Monthly spending cap" hint='This is enforced before the backend accepts a new billable job.'>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}><TextInput type="number" min="0" step="1" value={cap} onChange={v=>setGroup('spending','monthly_spending_cap_usd',Number(v))} mono width={180} /><span style={{ fontFamily: wf.sans, fontSize: 12, color: wf.ink3 }}>USD / month</span></div>
        </SettingsRow>
        <SettingsRow title="Usage alerts" hint="Configure email, Slack, or webhook alerts at cap thresholds. Delivery hooks are stored now; outbound sending can be added later.">
          <div style={{ display: 'grid', gap: 8 }}>
            <ToggleInput checked={spending.usage_alerts_enabled} onChange={v=>setGroup('spending','usage_alerts_enabled',v)} label="Enable usage alerts" />
            {alerts.map((alert, i) => (
              <div key={i} style={{ display: 'grid', gridTemplateColumns: '90px 120px 1fr auto auto', gap: 8, alignItems: 'center' }}>
                <TextInput type="number" min="1" max="100" step="1" value={alert.threshold} onChange={v=>setAlert(i,'threshold',Number(v))} mono />
                <SelectInput value={alert.channel || 'email'} onChange={v=>setAlert(i,'channel',v)} options={['email','slack','webhook']} />
                <TextInput value={alert.target || ''} onChange={v=>setAlert(i,'target',v)} placeholder="target" />
                <ToggleInput checked={alert.enabled !== false} onChange={v=>setAlert(i,'enabled',v)} label="on" />
                <span onClick={()=>removeAlert(i)} style={{ fontFamily: wf.mono, fontSize: 11, color: '#c33', cursor: 'pointer' }}>remove</span>
              </div>
            ))}
            <span onClick={addAlert} style={{ fontFamily: wf.mono, fontSize: 11, color: 'var(--accent)', cursor: 'pointer' }}>+ add alert threshold</span>
          </div>
        </SettingsRow>
      </div>
      <div style={{...dashStyles.card, marginTop: 14, border: '1px solid #f0c4c4', background: '#fdf6f6'}}>
        <div style={{ fontFamily: wf.sans, fontSize: 16, fontWeight: 600, marginBottom: 4, color: '#7a1f1f' }}>Danger zone</div>
        <div style={{ fontFamily: wf.sans, fontSize: 12.5, color: '#7a1f1f', marginBottom: 6 }}>Account deletion is intentionally not wired until export, cancellation, and retention policies are finalized.</div>
        <SettingsRow title="Delete account" hint="Not available yet. Revoke individual API keys from API keys if something is compromised.">
          <span style={{
            display: 'inline-flex', fontFamily: wf.mono, fontSize: 10.5, letterSpacing: 0.6,
            textTransform: 'uppercase', color: '#7a1f1f', background: '#fff',
            border: '1px solid #f0c4c4', borderRadius: 999, padding: '4px 8px',
          }}>Not available in beta</span>
        </SettingsRow>
      </div>
      {dirty && <div style={{ position: 'sticky', bottom: 14, marginTop: 14, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, padding: 12, borderRadius: 10, border: '1px solid #ead49a', background: '#fffaf0', boxShadow: '0 12px 34px rgba(11,13,16,0.12)' }}>
        <div style={{ fontFamily: wf.sans, fontSize: 13, color: '#6f4b00' }}>You have unsaved dashboard settings. Save before leaving this page.</div>
        {saveButton()}
      </div>}
      </>}
    </DashChrome>
  );
};
window.DashJobs = DashJobs;
window.DashSettings = DashSettings;
