// Represents: admin.job.index · Admin/JobController — see docs/ISYSTEM_REFERENCE.md §Batch 3 "Jobs"
/* ====================================================================
   JOBS — Settings ▾ rebuild (Batch 3), Hrs* shell.  GAP FILL: no
   prototype page existed for this screen; built from the reference only.
   ====================================================================
   Real screen: `GET /job` (`admin.job.index`, routes/admin.php:1705, group
   `Route::name('job.')->prefix('job')` L1704) → Admin\JobController::index
   (L37; column map built in __construct L16-35). Siblings:
   `admin.job.rows` GET /job/rows/ (L1706, JobController::rows L62),
   `admin.job.kill` POST /job/kill/{id} (L1707, ::kill L172),
   `admin.job.show` GET /job/{id?} (L1708, ::show L159).
   Views: admin/generics/index.blade.php + admin/generics/filters/job.blade.php
   + public/js/pages/generic/job.js + banConfirm()/doBan() in public/js/custom.js.
   No FormRequest and no inline validation anywhere — kill() only does
   `Job::findOrFail($id)` (L177).

   WHAT THE TABLE ACTUALLY IS — the DB `jobs` table, NOT Horizon.
   Production runs its queues through Horizon/Redis, and the Redis connection
   carries no `table` key, so `Job` falls back to `config('queue.connections.
   <QUEUE_CONNECTION>.table', 'jobs')` = 'jobs' (Job.php:19-23, config/queue.php
   :16,39). The screen therefore only ever lists database-driver jobs and can
   legitimately sit empty while the Redis queues are saturated. Single table,
   no relations; `show()` bypasses the model entirely with `DB::table('jobs')`
   (JobController:161).

   COOPERATIVE KILL — the one real action on this screen.
   Kill posts to `admin.job.kill`, which runs `Job::kill()` =
   `setProperty('should_be_killed', true)` (Job.php:60-63, 30-39): it
   unserializes the command object inside `jobs.payload`, sets the flag, and
   re-serializes the row. Nothing is signalled, nothing is dequeued. Only jobs
   whose own code polls `getProperty('should_be_killed')` mid-run actually
   stop — the known pollers are CommissionsController.php:115,581,
   BusinessReportController.php:591, PlayersReportController.php:465,
   User.php:856, Player.php:436, CouponMongo.php:1479 and TestJob.php:31.
   Anything else keeps running to completion with the flag set and unread.
   On a non-database queue driver `BaseJob::kill()` throws instead
   (BaseJob.php:90-114). This page models all of that: the confirm modal
   states plainly whether the target class is a known poller.

   FAITHFUL ABSENCES (brief §3 — nothing added):
   - NO failed_jobs UI, anywhere on the platform. `failed_jobs`
     (config/queue.php:100) is read by no admin route and no controller, so
     there is no Failed tab, no retry, no purge, no delete, no bulk action —
     Kill is the entire action surface. This rebuild says so out loud in the
     "Not on this screen" strip rather than inventing a Failed tab.
     // <!-- SUGGESTION: a read-only Failed jobs list (class, queue, exception,
     //      failed_at) plus a single Retry would close the biggest hole in
     //      queue observability — today a failed job is invisible to operators. -->
   - NO filters: filters/job.blade.php renders only a Search button (L1-13)
     and the hidden #banModal (L16-34); the whole per-column search block is
     commented out in rows() (JobController:78-118). The Search button here
     keeps its real semantics — it re-runs the same unfiltered query, i.e. a
     reload — and says so in its tip.
     // <!-- SUGGESTION: the column map already exists — un-comment the search
     //      block and add Class + Queue + "reserved only" inputs; on a busy
     //      queue an unfilterable list of every job is unusable. -->
   - NO create/edit: `'no_create' => true` (JobController:54).
   - NO KPIs/totals: iTotalRecords = iTotalDisplayRecords = plain Job::count()
     (JobController:121,150-151) — surfaced by the pager, nothing else.
   - NO export: the generic view loads the DataTables-buttons CDN bundles
     (index.blade.php:177-183) but configures no buttons.
   - NO status column: the reference is explicit that `attempts` and
     `reserved_at` are raw Laravel queue fields and no status enum exists, so
     none is invented here — a null `reserved_at` renders as an em dash with
     its meaning in the tip, not as a fabricated "Queued" chip.

   DIVERGENCES implemented as evident intent (known-bug policy):
   1. Dead sort arrows — order is hardcoded `jobs.id DESC` (JobController:76,
      124) and the order-mapping loop is commented out, so every header except
      Actions renders a sort arrow that does nothing (job.js:32). Sorting works
      here, default ID desc. Params stays unsorted: it is a JSON blob and
      ordering it means nothing.
      // <!-- SUGGESTION: restore the order-mapping loop in rows() so the sort
      //      arrows the client already draws actually order the query. -->
   2. Kill modal copy — the shared banConfirm() modal is Commission-Payments
      copy-paste residue: title literally 'Pay', body 'Are you sure you want to
      kill this payment?', a dead full['period'] param and a commented can_pay
      guard (job.js:36-39); the view data even passes
      'route_name_prefix' => 'admin.commission_payment.' (JobController:53).
      This rebuild uses the evident intent — a job-kill confirmation.
      // <!-- SUGGESTION: give the Jobs screen its own confirm modal and drop
      //      the commission_payment route prefix from the view data. -->
   3. Escaping — progress and params are injected as raw HTML by DataTables
      (`<nobr>` wrapper, params = the full command JSON), so job payload
      content is unescaped on the live screen. React escapes here by default.
      // <!-- SUGGESTION: escape the progress/params cells server-side; the
      //      params dump is attacker-influenced for any job carrying user input. -->

   HONEST NOTES kept visible rather than fixed:
   - `admin.job.show` has no permission check (JobController:159-170) while
     index/rows/kill all re-check isadmin() — any authenticated backoffice user
     can poll `GET /job/{id}` for {result, job_id, progress}. It looks
     deliberate: it backs the global export-progress poller in
     footer.blade.php:465-485, which runs for non-superadmin roles too.
   - rows() `echo`es json_encode(utf8ize($resp)) instead of returning a
     response (JobController:156) and sEcho is always 0.
   - Route-order quirk: `GET /job/{id?}` is registered last with an optional
     param, so any other `GET /job/<x>` falls through to show().

   LABELS: page title is `Str::plural(__('backend.job'))` and the column
   headers are backend.class / reserved_at / attempts / progress / params —
   none of those keys exist in any committed lang file (runtime translations
   load from the gitignored storage/lang/), so the live screen renders raw
   keys. Operator-facing labels are written here per the label policy and
   marked with a "label inferred" JSX comment at each column. Only
   `backend.id` = "ID" and
   `backend.search_button` = "Search" resolve for real
   (public/default-lang/en/backend.php L82, L86).

   Row shape is the real `jobs` table — id / queue / payload / attempts /
   reserved_at / available_at / created_at. The screen displays only ID,
   Class, Reserved at, Attempts, Progress and Params, so queue /
   available_at / created_at are carried on the record but not rendered.
   // <!-- SUGGESTION: show `queue` and `available_at` — the two fields an
   //      operator needs to tell "stuck behind a backlog" from "scheduled
   //      later", both already in the table and both already unused. -->

   Demo session = Super admin "admin" (level 0), the only role that can reach
   this screen. All new top-level names are hsj/Hsj/HSJ-prefixed except the
   required page component `SetJobs`.
   ==================================================================== */

const { useState: hsjUseState, useMemo: hsjUseMemo } = React;

/* ---------- deterministic PRNG (same shape as the other Host pages) ---------- */
const hsjRand = (seed) => () => {
  seed = (seed + 0x6D2B79F5) | 0;
  let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};

const hsjPad = (n) => String(n).padStart(2, "0");

/* JobController:141 formats reserved_at as `d/m/y H:i:s` (two-digit year,
   24-hour clock, everything padded) and leaves it null when unreserved. */
const hsjDT = (ts) => {
  if (!ts) return null;
  const d = new Date(ts);
  return `${hsjPad(d.getDate())}/${hsjPad(d.getMonth() + 1)}/${String(d.getFullYear()).slice(-2)} ${hsjPad(d.getHours())}:${hsjPad(d.getMinutes())}:${hsjPad(d.getSeconds())}`;
};

/* Frozen "now" so the generated queue is identical on every load. */
const HSJ_NOW = new Date(2026, 7, 7, 14, 42, 17).getTime();

/* ---------- the job catalogue ----------
   `killable` = the class polls getProperty('should_be_killed') mid-run, i.e.
   a Kill on it will actually stop the work. The pollers are the ones the
   reference enumerates; every other class ignores the flag and runs to
   completion. `inferred` marks a class name the reference does not spell out:
   it names the export/report POLLERS (controller call sites) but never the
   job class that wraps them, so ExportJob is a stand-in. */
const HSJ_JOBS = [
  {
    cls: "App\\Jobs\\ExportJob", killable: true, inferred: true, reports: true,
    props: (r) => {
      const kind = HSJ_REPORTS[Math.floor(r() * HSJ_REPORTS.length)];
      return { report: kind.key, skin_id: 1 + Math.floor(r() * 9), user_id: 1, from: "2026-07-01", to: "2026-07-31", format: r() > 0.35 ? "xlsx" : "csv", rows_expected: kind.rows };
    },
  },
  { cls: "App\\Jobs\\TestJob", killable: true, reports: true, props: (r) => ({ iterations: 20000, sleep_ms: 50 + Math.floor(r() * 100) }) },
  { cls: "App\\Jobs\\CustomerIoIdentifyJob", killable: false, props: (r) => ({ player_id: 4600000 + Math.floor(r() * 120000), skin_id: 1 + Math.floor(r() * 9), traits: ["email", "created_at", "skin_id", "currency"] }) },
  { cls: "App\\Jobs\\SendTelegramMessageJob", killable: false, props: (r) => ({ topic: HSJ_TOPICS[Math.floor(r() * HSJ_TOPICS.length)], chat_id: -1002194883310, text_length: 120 + Math.floor(r() * 900) }) },
  { cls: "App\\Jobs\\OnAimSendEventJob", killable: false, props: (r) => ({ event: HSJ_EVENTS[Math.floor(r() * HSJ_EVENTS.length)], player_id: 4600000 + Math.floor(r() * 120000), amount: Math.round(r() * 50000) / 100, currency: "ARS" }) },
  { cls: "App\\Jobs\\LogLaunchUrlTimelineJob", killable: false, props: (r) => ({ launch_url_id: 1 + Math.floor(r() * 13), action: r() > 0.5 ? "updated" : "created", user_id: 1 }) },
];

/* Report kinds behind the export job — one per killable poller the reference
   lists (CommissionsController, BusinessReportController, PlayersReportController,
   CouponMongo, User/Player). */
const HSJ_REPORTS = [
  { key: "commissions", rows: 18420, label: "Commissions" },
  { key: "business_report", rows: 96500, label: "Business report" },
  { key: "players_report", rows: 41300, label: "Players report" },
  { key: "coupons", rows: 210400, label: "Sport coupons" },
  { key: "users", rows: 7650, label: "Users" },
];
const HSJ_TOPICS = ["errors", "deployments", "monitoring", "business", "critical"];
const HSJ_EVENTS = ["deposit", "bet", "win", "registration"];

/* ---------- rows ----------
   Real `jobs` columns: id, queue, payload, attempts, reserved_at,
   available_at, created_at. `payload` is the Laravel envelope; `cmd` models
   the unserialized command object that lives inside payload.data.command as a
   PHP-serialized string — that object is what Job::getName() and
   Job::getProperty() read, and what the Params cell dumps wholesale.
   `queue` is 'default' throughout: per-job queue routing is not something the
   reference resolves, and the screen never displays the column anyway. */
const hsjGenRows = () => {
  const r = hsjRand(20260807);
  const rows = [];
  let id = 918744;
  let t = HSJ_NOW - 90 * 1000;
  for (let i = 0; i < 64; i++) {
    const def = HSJ_JOBS[Math.floor(r() * HSJ_JOBS.length)];
    /* Reserved = a worker currently holds it. Laravel bumps attempts on
       reservation, so a reserved row always has attempts >= 1. */
    const reserved = r() < 0.22;
    const attempts = reserved ? 1 + Math.floor(r() * 3) : (r() < 0.12 ? 1 : 0);
    const createdAt = t;
    const availableAt = createdAt + (attempts > 1 ? 90 * 1000 : 0); // retry backoff
    const reservedAt = reserved ? createdAt + Math.floor(r() * 40) * 1000 : null;

    const cmd = { ...def.props(r) };
    /* BaseJob::$progress / $current (BaseJob.php:30-32) exist on every job;
       only long-running report/export jobs ever move them off the defaults. */
    cmd.progress = 0;
    if (def.reports && reserved) {
      cmd.progress = Math.round(r() * 9800) / 100;
      const total = cmd.rows_expected || cmd.iterations || 10000;
      cmd.current = `${Math.floor((total * cmd.progress) / 100)}/${total}`;
    }
    /* Two rows arrive with the flag already written — what the payload looks
       like after someone pressed Kill on a previous page load. */
    if (i === 3 || i === 17) cmd.should_be_killed = true;

    rows.push({
      id,
      queue: "default",
      attempts,
      reserved_at: reservedAt,
      available_at: availableAt,
      created_at: createdAt,
      payload: {
        uuid: `9f${(id * 7919).toString(16)}-4c1a-4f8e-b0d2-${(id * 104729).toString(16).slice(-12)}`,
        displayName: def.cls,
        job: "Illuminate\\Queue\\CallQueuedHandler@call",
        maxTries: 3,
        data: { commandName: def.cls, command: "O:" + def.cls.length + ':"' + def.cls + '":…' },
      },
      cmd,
      killable: def.killable,
      inferred: !!def.inferred,
    });
    id -= 1 + Math.floor(r() * 4);
    t -= (20 + Math.floor(r() * 400)) * 1000;
  }
  return rows;
};

const HSJ_SEED_ROWS = hsjGenRows();

/* ---------- cell renderers ---------- */

/* Job::getName() returns payload.displayName (Job.php:55-58). Split so the
   namespace recedes and the class name reads first. */
const HsjClassCell = ({ row }) => {
  const parts = row.payload.displayName.split("\\");
  const cls = parts.pop();
  return (
    <span className="hsj-cls">
      <span className="hsj-cls__ns">{parts.join("\\")}\</span>
      <b className="hsj-cls__n">{cls}</b>
      {row.inferred && <span className="hsj-inferred" title="Class name inferred — the reference names the export pollers, not the job class that wraps them">inferred</span>}
    </span>
  );
};

/* JobController:132-143 — round(progress, 2) . ' %', with '( current )'
   appended when set. The bar is presentation over the same number. */
const hsjProgressText = (cmd) => {
  const p = Math.round((cmd.progress || 0) * 100) / 100;
  return `${p} %${cmd.current ? ` ( ${cmd.current} )` : ""}`;
};

const HsjProgressCell = ({ row }) => {
  const p = Math.max(0, Math.min(100, Number(row.cmd.progress) || 0));
  const idle = p === 0 && !row.cmd.current;
  return (
    <span className="hsj-prog">
      <span className={`hsj-prog__track${idle ? " hsj-prog__track--idle" : ""}`}>
        <span className="hsj-prog__fill" style={{ width: `${p}%` }} />
      </span>
      <span className="hsj-prog__t">{hsjProgressText(row.cmd)}</span>
    </span>
  );
};

/* Job::getProperty() with no key = json_encode of the whole unserialized
   command object (Job.php:41-53). One line in the cell, pretty-printed in the
   row detail. */
const hsjParamsJson = (cmd) => JSON.stringify(cmd);
const hsjParamsPretty = (cmd) => JSON.stringify(cmd, null, 2);

const HsjParamsCell = ({ row }) => (
  <span className="hsj-params">
    {row.cmd.should_be_killed && (
      <span className="hsj-killflag" title="should_be_killed = true — the flag is written into jobs.payload; the job stops only if its own code polls it">
        <Icon name="zap" size={10} /> kill flag set
      </span>
    )}
    <code className="hsj-json">{hsjParamsJson(row.cmd)}</code>
  </span>
);

/* ---------- kill confirmation ----------
   Replaces the shared banConfirm() modal, whose title is literally 'Pay' and
   whose body asks about "this payment" (job.js:36-39) — see divergence 2. */
const HsjKillModal = ({ row, onClose, onConfirm }) => {
  if (!row) return null;
  const already = !!row.cmd.should_be_killed;
  return (
    <div className="bp-modal-scrim hsj-scrim" onClick={onClose}>
      <div className="hsj-modal" onClick={(e) => e.stopPropagation()}>
        <div className="hsj-modal__head">
          <div className="hsj-modal__ic"><Icon name="zap" size={17} /></div>
          <div className="hsj-modal__title">
            Kill job
            <span className="hsj-modal__sub">#{row.id} · {row.payload.displayName.split("\\").pop()}</span>
          </div>
          <button className="hsj-modal__x" onClick={onClose} title="Close"><Icon name="x" size={13} /></button>
        </div>
        <div className="hsj-modal__body">
          <p>Are you sure you want to kill this job?</p>
          <p className="hsj-modal__note">
            Killing writes <code>should_be_killed = true</code> into the job&rsquo;s serialized command inside{" "}
            <code>jobs.payload</code>. Nothing is signalled and nothing is removed from the queue — the running
            worker has to notice the flag on its own and stop.
          </p>
          {row.killable ? (
            <div className="hsj-verdict hsj-verdict--ok">
              <Icon name="check" size={13} />
              <span><b>{row.payload.displayName.split("\\").pop()}</b> polls the flag while it runs, so it should stop at its next checkpoint.</span>
            </div>
          ) : (
            <div className="hsj-verdict hsj-verdict--warn">
              <Icon name="alert" size={13} />
              <span><b>{row.payload.displayName.split("\\").pop()}</b> never reads the flag. The payload will be updated and the job will keep running to completion — killing it has no effect beyond the record.</span>
            </div>
          )}
          {already && (
            <div className="hsj-verdict hsj-verdict--muted">
              <Icon name="info" size={13} />
              <span>The flag is already set on this job. Pressing Kill again just rewrites the same value.</span>
            </div>
          )}
          <p className="hsj-modal__note hsj-modal__note--last">
            This only works on the database queue driver — on any other driver <code>BaseJob::kill()</code> throws
            instead of writing the flag.
          </p>
        </div>
        <div className="hsj-modal__foot">
          <button className="rpt-btn hsj-btn hsj-btn--ghost" onClick={onClose}>Cancel</button>
          <button className="rpt-btn rpt-btn--danger hsj-btn" onClick={onConfirm}><Icon name="zap" size={13} /> Kill job</button>
        </div>
      </div>
    </div>
  );
};

/* ---------- the honest-absence strip ----------
   The reference is explicit that failed_jobs is surfaced nowhere on the
   platform and that Kill is the entire action surface. Stated, not invented
   around. */
const HsjAbsences = () => (
  <div className="hsj-absence">
    <div className="hsj-absence__h"><Icon name="info" size={13} /> Not on this screen — and not anywhere else in the backoffice</div>
    <ul className="hsj-absence__l">
      <li><b>No failed jobs.</b> The <code>failed_jobs</code> table is read by no admin route and no controller. A job that throws its final attempt disappears from this list and is visible only in the logs — there is no Failed tab to add here, so none is shown.</li>
      <li><b>No retry, no delete, no purge, no bulk action.</b> Kill is the only endpoint that writes; it sets a flag and nothing else.</li>
      <li><b>No filters and no export.</b> The filter bar holds a single Search button and the per-column search is commented out server-side; the export libraries load but no export button is configured.</li>
      <li><b>Horizon is not here.</b> This lists the database <code>jobs</code> table only. With queues running on Horizon/Redis the list can be empty while the platform is busy — empty means &ldquo;no database-driver jobs&rdquo;, not &ldquo;nothing running&rdquo;.</li>
    </ul>
  </div>
);

/* ---------- columns ----------
   Display order per JobController::__construct L16-35 / job.js:31.
   Sorters exist because the live arrows are dead (divergence 1); Params is
   left unsorted — ordering a JSON blob is meaningless. */
const HSJ_SORTERS = {
  id: (r) => r.id,
  name: (r) => r.payload.displayName.toLowerCase(),
  reserved_at: (r) => r.reserved_at || 0,
  attempts: (r) => r.attempts,
  progress: (r) => Number(r.cmd.progress) || 0,
};

const HSJ_COLUMNS = (onKill) => [
  { key: "id", label: "ID", sortable: true, width: 92, firstDir: "desc" },
  /* label inferred */
  { key: "name", label: "Class", sortable: true, render: (r) => <HsjClassCell row={r} /> },
  /* label inferred */
  {
    key: "reserved_at", label: "Reserved at", sortable: true, width: 150,
    render: (r) => r.reserved_at
      ? <span className="hsj-res">{hsjDT(r.reserved_at)}</span>
      : <span className="hsj-none" title="reserved_at is null — no worker has picked this job up yet">—</span>,
  },
  /* label inferred */
  {
    key: "attempts", label: "Attempts", align: "center", sortable: true, width: 104,
    render: (r) => <span className={r.attempts > 1 ? "hsj-att hsj-att--retry" : "hsj-att"}>{r.attempts}</span>,
  },
  /* label inferred */
  { key: "progress", label: "Progress", sortable: true, width: 220, render: (r) => <HsjProgressCell row={r} /> },
  /* label inferred */
  { key: "params", label: "Params", render: (r) => <HsjParamsCell row={r} /> },
  {
    key: "actions", label: "Actions", align: "center", width: 92,
    render: (r) => (
      <button className="hsj-kill" title="Kill" onClick={(e) => { e.stopPropagation(); onKill(r); }}>
        <Icon name="zap" size={13} /> Kill
      </button>
    ),
  },
];

/* ==================================================================
   Page component — name required by the orchestrator's route wiring.
   ================================================================== */
const SetJobs = () => {
  window.useLocale && window.useLocale();
  const [rows, setRows] = hsjUseState(HSJ_SEED_ROWS);
  /* Server order is hardcoded jobs.id DESC (JobController:76,124). */
  const [sort, setSort] = hsjUseState({ key: "id", dir: "desc" });
  /* job.js:17-19 — pageLength 50, lengthMenu [5,10,25,50]. */
  const [page, setPage] = hsjUseState(0);
  const [pageSize, setPageSize] = hsjUseState(50);
  const [killing, setKilling] = hsjUseState(null);
  const [reloads, setReloads] = hsjUseState(0);

  const sorted = hsjUseMemo(() => {
    const get = HSJ_SORTERS[sort.key] || HSJ_SORTERS.id;
    return [...rows].sort((a, b) => {
      const av = get(a), bv = get(b);
      const d = typeof av === "string" ? av.localeCompare(bv) : av - bv;
      return sort.dir === "asc" ? d : -d;
    });
  }, [rows, sort]);

  const view = sorted.slice(page * pageSize, (page + 1) * pageSize);

  /* POST /job/kill/{id} → Job::kill() → setProperty('should_be_killed', true):
     the payload is rewritten, the row stays in the queue. */
  const doKill = () => {
    const target = killing;
    setKilling(null);
    setRows((rs) => rs.map((r) => r.id === target.id ? { ...r, cmd: { ...r.cmd, should_be_killed: true } } : r));
    hrsToast(
      `Kill flag written · job #${target.id}`,
      target.killable
        ? `should_be_killed = true saved into jobs.payload. ${target.payload.displayName.split("\\").pop()} polls the flag and should stop at its next checkpoint.`
        : `should_be_killed = true saved into jobs.payload. ${target.payload.displayName.split("\\").pop()} does not poll the flag, so it will run to completion regardless.`
    );
  };

  const reserved = rows.filter((r) => r.reserved_at).length;

  return (
    <HrsShell
      title="Jobs"                    /* label inferred — Str::plural(__('backend.job')) resolves nowhere and renders as `backend.jobs` live */
      subtitle="Queued and in-flight jobs sitting in the database queue table."
      gate={<>
        No <code>checkUserBoPerm</code> gate on this entry — access is purely role-based. The sidebar item renders
        under <code>@if (isadmin())</code>, i.e. <b>Super Admin only</b> (<code>user_level == 0</code>), and{" "}
        <code>index</code>, <code>rows</code> and <code>kill</code> each re-check <code>isadmin()</code> server-side.{" "}
      </>}
      gateNote={<>
        One hole: <code>admin.job.show</code> (<code>GET /job/&#123;id&#125;</code>) skips that check, so any
        authenticated backoffice user can read <code>&#123;result, job_id, progress&#125;</code> for any job id. It
        looks deliberate — the global footer progress poller runs for non-superadmin roles and polls it once a second.
      </>}
      explainer={{
        title: "What this screen is, in plain English",
        bullets: [
          <>This is the raw Laravel <b>database queue table</b> (<code>jobs</code>) — not Horizon. Production runs its queues on Horizon/Redis, and the Redis connection has no table mapping, so this list only ever contains database-driver jobs. <b>An empty list does not mean the platform is idle.</b></>,
          <><b>Kill is cooperative, not a hard kill.</b> It writes <code>should_be_killed = true</code> into the job&rsquo;s serialized command inside <code>jobs.payload</code> — no signal, no dequeue, no process touched. The job stops only if its own code checks that flag while it runs; the report/export and coupon jobs do, the notification jobs (Customer.io, Telegram, OnAim, timeline logs) do not, and those will finish normally with the flag set and unread. It also only works on the database queue driver — elsewhere <code>BaseJob::kill()</code> throws.</>,
          <><b>Reserved at</b> is the moment a worker claimed the job; empty means it is still waiting. <b>Attempts</b> is Laravel&rsquo;s retry counter — anything above 1 means the job already failed and was released back. There is no status column on this screen because the real table has no status field.</>,
          <><b>Progress</b> and <b>Params</b> both come out of the same serialized command object: progress is <code>BaseJob::$progress</code> (plus <code>$current</code> when the job sets it), params is the whole object dumped as JSON. Only long-running export/report jobs move progress off zero.</>,
          <><b>Failed jobs are not visible anywhere in this backoffice.</b> The <code>failed_jobs</code> table has no route, controller or screen, so a job that exhausts its attempts simply vanishes from this list. That is a real gap in the platform, not a missing tab in this rebuild.</>,
        ],
      }}
      actions={
        <button className="rpt-btn rpt-btn--blue hsj-reload" onClick={() => { setReloads((n) => n + 1); setPage(0); hrsToast("Job list reloaded", `${rows.length} jobs in the queue · ${reserved} currently reserved by a worker.`); }}>
          <Icon name="search" size={14} /> Search
          <Tip size={12}>
            The real filter bar contains this button and nothing else — there are no filter inputs, and the
            per-column search is commented out server-side. Pressing it re-runs the same unfiltered query, so
            in practice it is a reload. Kept with its real label and its real effect.
          </Tip>
        </button>
      }
    >
      {/* No HrsKpis: the controller computes no totals — iTotalRecords is a plain Job::count(). */}
      {/* No HrsFilters: the real screen has zero filter inputs — see the header. */}
      {/* No HrsExport: no export button is configured on the real screen. */}
      <div className="hsj" key={reloads}>
        <div className="hsj-queueline">
          <span><b>{hrsInt(rows.length)}</b> jobs in <code>jobs</code></span>
          <span className="hsj-queueline__sep" />
          <span><b>{hrsInt(reserved)}</b> reserved by a worker</span>
          <span className="hsj-queueline__sep" />
          <span><b>{hrsInt(rows.length - reserved)}</b> waiting</span>
          <Tip size={12}>
            Counted from the rows on this page&rsquo;s dataset — the real screen shows no totals at all beyond the
            DataTables record count, which is a plain <code>Job::count()</code>. Reserved / waiting is just
            <code> reserved_at</code> being set or null, not a status field.
          </Tip>
        </div>

        <HrsTable
          columns={HSJ_COLUMNS(setKilling)}
          rows={view}
          sort={sort}
          onSort={(next) => { setSort(next); setPage(0); }}
          rowKey="id"
          empty="No data available in table" /* DataTables' own empty string — and see the note below on what empty means here */
          rowDetail={(r) => (
            <div className="hsj-detail">
              <div className="hsj-detail__h">
                <span>Params — <code>Job::getProperty()</code> with no key, i.e. the whole unserialized command object</span>
                <span className="hsj-detail__meta">
                  queue <code>{r.queue}</code> · created <code>{hsjDT(r.created_at)}</code> · available <code>{hsjDT(r.available_at)}</code>
                  <Tip size={12}>
                    <code>queue</code>, <code>created_at</code> and <code>available_at</code> are real columns on the
                    <code> jobs</code> table that the live screen never displays. Shown here only as context for the
                    payload dump — no new column was added to the table itself.
                  </Tip>
                </span>
              </div>
              <pre className="hsj-detail__json">{hsjParamsPretty(r.cmd)}</pre>
              <div className="hsj-detail__env">
                <span>uuid <code>{r.payload.uuid}</code></span>
                <span>maxTries <code>{r.payload.maxTries}</code></span>
                <span>handler <code>{r.payload.job}</code></span>
              </div>
            </div>
          )}
          renderCard={(r) => <>
            <div className="hrs-card__top">
              <span className="hsj-card__id">#{r.id}</span>
              <span className="hsj-card__cls">{r.payload.displayName.split("\\").pop()}</span>
            </div>
            <div className="hsj-card__prog"><HsjProgressCell row={r} /></div>
            <div className="hrs-card__grid">
              <span>Reserved at</span><b>{r.reserved_at ? hsjDT(r.reserved_at) : "—"}</b>
              <span>Attempts</span><b>{r.attempts}</b>
            </div>
            <details className="hsj-card__more">
              <summary>Params{r.cmd.should_be_killed ? " · kill flag set" : ""}</summary>
              <pre className="hsj-detail__json">{hsjParamsPretty(r.cmd)}</pre>
            </details>
            <button className="hsj-kill hsj-kill--card" onClick={() => setKilling(r)}><Icon name="zap" size={13} /> Kill</button>
          </>}
        />
        <HrsPager
          page={page}
          pageSize={pageSize}
          total={rows.length}
          onPage={setPage}
          onPageSize={(n) => { setPageSize(n); setPage(0); }}
          sizes={[5, 10, 25, 50]}
        />
        <HsjAbsences />
      </div>

      <HsjKillModal row={killing} onClose={() => setKilling(null)} onConfirm={doKill} />
    </HrsShell>
  );
};

window.SetJobs = SetJobs;
