<?php

declare(strict_types=1);

/**
 * Dashboard (SPEC §11.1): the pulse of the board: pending applications,
 * page-sysop alert with the minimal admin chat pane, who's online, today's
 * traffic, recent events, cron health.
 */

require __DIR__ . '/_lib.php';
$me = admin_require();
$db = atdt_db();
$settings = atdt_settings();

use Atdt\BbsSession;
use Atdt\Chat;
use Atdt\Cron;
use Atdt\ScreenEngine;

// --- Ajax: chat pane rows after an id (admin side of §9.6). -----------------
if (($_GET['ajax'] ?? '') === 'chat') {
    header('Content-Type: application/json; charset=utf-8');
    // The console heartbeat (1.5.5): callers on the private line are told
    // when this stops beating -- the sysop stepped away from the wire.
    $settings->set('console_seen_at', now_utc());
    $after = (int)($_GET['after'] ?? 0);
    $rows = $db->all('SELECT * FROM chat_msgs WHERE id > ? ORDER BY id LIMIT 100', [$after]);
    echo json_encode(['rows' => array_map(fn(array $r) => [
        'id' => (int)$r['id'],
        'kind' => (string)$r['kind'],
        'line' => admin_chat_line($r),
        // Public-room lines can be moderated away (1.6.5).
        'del' => $r['to_node'] === null && in_array((string)$r['kind'], ['chat', 'broadcast'], true),
    ], $rows)]);
    exit;
}

/** Node number of a caller currently on the wire, or 0. */
function live_node(\Atdt\Db $db, int $node): int
{
    if ($node <= 0) {
        return 0;
    }
    $row = $db->one(
        'SELECT node FROM sessions WHERE active = 1 AND kick = 0 AND user_id IS NOT NULL AND node = ?',
        [$node]
    );
    return $row === null ? 0 : $node;
}

/**
 * The console walks away from a private line (1.5.5): drop the hang-up
 * marker so that caller's screen says the sysop left instead of leaving
 * them talking to a dead wire. Only fires when the aim really moves off
 * a still-connected caller.
 */
function line_hangup(\Atdt\Db $db, array $me, int $prev, int $next): void
{
    if ($prev > 0 && $prev !== $next && live_node($db, $prev) === $prev) {
        Chat::post($db, 0, (string)$me['handle'], 'hung up the private line', 'sysophup', $prev);
    }
}

// --- POST actions ------------------------------------------------------------
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
    $settings->set('console_seen_at', now_utc());
    switch ((string)($_POST['action'] ?? '')) {
        case 'talkto':
            // Aim the wire (owner round 5): the target lives SERVER-side in
            // the console session, so the flow works with no JavaScript at
            // all; the say form renders pre-aimed with a loud banner.
            $want = max(0, (int)($_POST['node'] ?? 0));
            $node = live_node($db, $want);
            if ($want > 0 && $node === 0) {
                flash_set('err', "Node $want is not on the wire any more.");
            }
            line_hangup($db, $me, (int)($_SESSION['talk_node'] ?? 0), $node);
            $_SESSION['talk_node'] = $node;
            admin_redirect('index.php#chat');

        case 'say':
            $node = max(0, (int)($_POST['node'] ?? 0));
            $body = Chat::clean((string)($_POST['body'] ?? ''));
            $err = '';
            if ($node > 0 && live_node($db, $node) === 0) {
                $err = "Node $node hung up - nothing was sent. Back to the public room.";
                $node = 0;
                $_SESSION['talk_node'] = 0;
            } elseif ($body !== '') {
                // node 0 = a normal line into the public chat room; a node
                // number = a PRIVATE sysop line that breaks into that caller's
                // screen (owner round 3). Systemwide banners are 'bcast'.
                line_hangup($db, $me, (int)($_SESSION['talk_node'] ?? 0), $node);
                Chat::post($db, 0, (string)$me['handle'], $body, $node > 0 ? 'sysop' : 'chat', $node > 0 ? $node : null);
                $_SESSION['talk_node'] = $node; // the select is the truth; keep the banner honest
            }
            if (($_POST['ajax'] ?? '') === '1') {
                header('Content-Type: application/json; charset=utf-8');
                echo json_encode(['ok' => $err === '', 'err' => $err, 'talk_node' => (int)($_SESSION['talk_node'] ?? 0)]);
                exit;
            }
            if ($err !== '') {
                flash_set('err', $err);
            }
            admin_redirect('index.php#chat');

        case 'bcast':
            $text = mb_strimwidth(trim(preg_replace('/[\x00-\x1f\x7f]/', '', (string)($_POST['text'] ?? '')) ?? ''), 0, 100);
            $minutes = max(1, min(10, (int)($_POST['minutes'] ?? 2)));
            if ($text !== '') {
                $settings->set('broadcast_msg', $text);
                $settings->set('broadcast_until', (string)(time() + $minutes * 60));
                $settings->set('broadcast_id', (string)time());
                admin_log("Broadcast for {$minutes}m: $text");
                flash_set('ok', "Broadcast is up for $minutes minute(s). Every caller sees it above their screen.");
            }
            admin_redirect('index.php');
        case 'avail':
            $on = (string)($_POST['on'] ?? '0') === '1';
            $settings->set('sysop_available', $on ? '1' : '0');
            admin_log('Marked SysOp ' . ($on ? 'available' : 'away'));
            admin_redirect('index.php');
        case 'clearpage':
            $settings->set('sysop_paged_at', '');
            $settings->set('sysop_paged_by', '');
            admin_redirect('index.php');
        case 'dismiss_notice':
            // The one-time upgrade note about theme copies.
            $settings->set('notice_theme_strings', '');
            admin_redirect('index.php');
        case 'kick':
            // Hang up a line (1.6.0): the flag lands on the session and
            // the very next request -- poll included -- ends the call.
            $node = (int)($_POST['node'] ?? 0);
            $row = $db->one('SELECT * FROM sessions WHERE active = 1 AND node = ?', [$node]);
            if ($row !== null) {
                $db->q('UPDATE sessions SET kick = 1 WHERE id = ?', [(int)$row['id']]);
                admin_log('Hung up node ' . $node);
                flash_set('ok', 'Node ' . $node . ' will be dropped on its next breath.');
            }
            admin_redirect('index.php');
        case 'delchat':
            // Moderate the public room (1.6.5): one line, gone for good.
            // Only public-room lines qualify; pages and private sysop
            // traffic are working records, not the room.
            $cid = (int)($_POST['id'] ?? 0);
            $row = $db->one(
                "SELECT * FROM chat_msgs WHERE id = ? AND to_node IS NULL AND kind IN ('chat', 'broadcast')",
                [$cid]
            );
            $ok = false;
            if ($row !== null) {
                $db->q('DELETE FROM chat_msgs WHERE id = ?', [$cid]);
                admin_log('Deleted a public chat line by ' . (string)$row['from_handle']
                    . ': "' . mb_strimwidth((string)$row['body'], 0, 40) . '"');
                $ok = true;
            }
            if (($_POST['ajax'] ?? '') === '1') {
                header('Content-Type: application/json; charset=utf-8');
                echo json_encode(['ok' => $ok]);
                exit;
            }
            flash_set($ok ? 'ok' : 'err', $ok ? 'The line is gone.' : 'That line is not in the public room.');
            admin_redirect('index.php#chat');
    }
    admin_redirect('index.php');
}

/** One chat row as console text (admin sees everything, incl. pages). */
function admin_chat_line(array $r): string
{
    $time = admin_stamp((string)$r['at'], 'H:i');
    $who = (int)$r['from_node'] === 0 ? $r['from_handle'] . '@console' : $r['from_handle'] . '@node' . $r['from_node'];
    $tag = match ((string)$r['kind']) {
        'page' => ' [PAGE]',
        'sysop' => (int)($r['to_node'] ?? 0) === 0 ? ' [private, to you]' : ' [private, to node ' . (int)$r['to_node'] . ']',
        'sysophup' => ' [node ' . (int)($r['to_node'] ?? 0) . ' was told]',
        default => '',
    };
    return sprintf('%s <%s>%s %s', $time, $who, $tag, (string)$r['body']);
}

// --- Gather the dashboard ----------------------------------------------------
$settings->set('console_seen_at', now_utc()); // eyes on the wire (1.5.5)
BbsSession::reap($db, $settings);
$pending = (int)$db->val("SELECT COUNT(*) FROM users WHERE status = 'pending'");
$pagedBy = trim((string)($settings->get('sysop_paged_by', '') ?? ''));
$pagedNode = preg_match('/\(node (\d+)\)$/', $pagedBy, $pm) ? (int)$pm[1] : 0;
$pagedAt = (string)($settings->get('sysop_paged_at', '') ?? '');
$available = $settings->bool('sysop_available', false);
// Straight from the DB: the migration that arms this may have run earlier
// THIS request, after the settings cache warmed (the sysop's first hit
// after an upload is exactly when the notice matters most).
$noticeStrings = trim((string)$db->val("SELECT v FROM settings WHERE k = 'notice_theme_strings'", [], ''));
$online = $db->all(
    'SELECT s.node, s.state, s.created_at, s.last_seen, u.handle
     FROM sessions s LEFT JOIN users u ON u.id = s.user_id
     WHERE s.active = 1 AND s.kick = 0 ORDER BY s.node'
);
// The aimed node survives in the console session; drop it if they hung up.
$talkNode = live_node($db, (int)($_SESSION['talk_node'] ?? 0));
$_SESSION['talk_node'] = $talkNode;
$handleByNode = [];
foreach ($online as $s) {
    if ($s['handle'] !== null) {
        $handleByNode[(int)$s['node']] = (string)$s['handle'];
    }
}
$callsToday = (int)$db->val('SELECT COUNT(*) FROM call_log WHERE on_at >= ?', [Cron::boardDayStartUtc($settings)]);
$postsToday = (int)$db->val('SELECT COUNT(*) FROM messages WHERE posted_at >= ?', [Cron::boardDayStartUtc($settings)]);
$events = $db->all('SELECT * FROM events ORDER BY id DESC LIMIT 12');
$chatRows = array_reverse($db->all('SELECT * FROM chat_msgs ORDER BY id DESC LIMIT 30'));
$chatMax = (int)$db->val('SELECT COALESCE(MAX(id), 0) FROM chat_msgs');
$stale = Cron::staleness($settings);
$cronKey = (string)(atdt_config()['cron_key'] ?? '');
$cronLine = '*/5 * * * * php ' . ATDT_ROOT . '/cron.php key=' . $cronKey;

admin_page_start('Dashboard', 'index');
?>

<?php if ($noticeStrings !== ''):
    // One-time upgrade note, armed by any release that adds or rewords
    // default-theme strings (owner's standing rule, 1.6.0): the board
    // never edits a SysOp's theme copies, so copies made before this
    // release miss the new text until they are touched up by hand. The
    // list below is keyed by the release that armed the notice.
    $noticeThemes = array_map('basename', array_map('dirname', glob(ATDT_ROOT . '/data/themes/*/strings.ini') ?: []));
    // The stored value is the OLDEST release the copies missed; every
    // list from that point forward applies (a board can climb several
    // rungs in one upload).
    $noticeAll = [
        '1.5.0' => [
            '<code>mail &gt; pick</code> and <code>mail &gt; pick_empty</code>: now mention <strong>S</strong>, the sent/inbox flip in the mail room.',
            '<code>set &gt; prompt</code>: now mentions <strong>E</strong>, the email row in the caller settings screen.',
        ],
        '1.6.0' => [
            '<code>matrix &gt; reset_ask</code>: reworded to <em>Reset for:</em> so the reset never reads as a bounce back to login.',
            '<code>matrix &gt; reset_note</code> <span class="hint">(new)</span>: the PASSWORD RESET banner on the reset screen.',
            '<code>matrix &gt; reset_sent_for</code> <span class="hint">(new)</span>: names the handle a reset letter went to.',
            '<code>matrix &gt; reset_sysop</code> <span class="hint">(new)</span>: tells a SysOp handle why no reset mail ever comes.',
            '<code>sysopchat &gt; split_top</code> and <code>split_bottom</code> <span class="hint">(new)</span>: the split-screen chat labels, if you switch the new chat style on.',
            '<code>list &gt; commands</code>: now mentions <strong>S</strong>, the thread-order flip in the message list.',
            '<code>auto &gt; prompt_edit</code> and <code>editor_title</code>: the wall paints fresh now - E opens an empty editor that replaces the old coat, capped at 8 lines.',
        ],
        '1.6.1' => [
            '<code>nua &gt; banned_handle</code> <span class="hint">(new)</span>: the refusal a prohibited handle gets at the application desk.',
            '<code>nua &gt; banned_bye</code> and <code>banned_ip</code> <span class="hint">(new)</span>: the goodbye after three prohibited names in a row, with the caller\'s address read back (<code>%IP</code>).',
        ],
        '1.6.5' => [
            '<code>menu &gt; area_missing</code> and <code>area_denied</code> <span class="hint">(new)</span>: what a caller sees when a menu item targets a message area that is gone, or one their access does not cover.',
        ],
        '1.7.0' => [
            '<code>mail &gt; read_more</code> <span class="hint">(new)</span>: the pager row under a long letter in the mail room.',
            '<code>list &gt; commands</code>: now mentions <strong>Home/End</strong>, the jump to the top or bottom of the message list.',
            '<code>nua &gt; bad_handle</code>: handles may carry <strong>[</strong> and <strong>]</strong> now, and the refusal says so.',
        ],
    ];
    $noticeItems = [];
    $reached = false;
    foreach ($noticeAll as $ver => $items) {
        $reached = $reached || $ver === $noticeStrings;
        if ($reached) {
            $noticeItems = array_merge($noticeItems, $items);
        }
    }
    if ($noticeItems === []) {
        $noticeItems = $noticeAll[array_key_last($noticeAll)];
    }
?>
<div style="position:fixed;inset:0;background:rgba(4,6,12,.72);z-index:90;display:flex;align-items:center;justify-content:center;padding:1rem">
  <div class="card" style="max-width:34rem;box-shadow:0 12px 48px rgba(0,0,0,.5)">
    <h2><?= h($noticeStrings) ?>: new default-theme strings arrived</h2>
    <p>This release added or reworded caller-facing theme strings. The
    shipped <code>default</code> theme already carries the new text - the
    board updates <em>only</em> the default, never your copies - so
    <?= $noticeThemes === [] ? 'your copies' : '<strong>' . h(implode(', ', $noticeThemes)) . '</strong>' ?>
    keep their old wording until you touch them up.</p>
    <p>On the <a href="themes.php">Themes</a> page, open Strings for each copy
    and bring over what you want (the shipped text is shown there for
    reference):</p>
    <ul>
      <?php foreach ($noticeItems as $item): ?><li><?= $item ?></li><?php endforeach; ?>
    </ul>
    <p class="hint">Skipping this breaks nothing: missing strings fall back
    to the shipped wording; reworded ones just stay old in your copies.</p>
    <form method="post" class="inline"><?= csrf_field() ?>
      <input type="hidden" name="action" value="dismiss_notice">
      <button class="btn">Got it</button></form>
  </div>
</div>
<?php endif; ?>

<?php if ($pagedBy !== ''): ?>
<div class="alert">
  <strong>The bell rang:</strong> <?= h($pagedBy) ?> paged you
  <?= $pagedAt !== '' ? 'at ' . h(admin_stamp($pagedAt, 'H:i')) : '' ?>.
  <form method="post" class="inline"><?= csrf_field() ?><input type="hidden" name="action" value="talkto"><input type="hidden" name="node" value="<?= $pagedNode ?>"><button class="btn small">Answer on their line</button></form>
  <form method="post" class="inline"><?= csrf_field() ?><input type="hidden" name="action" value="clearpage"><button class="btn small secondary">Dismiss</button></form>
</div>
<?php endif; ?>

<div class="cards">
  <div class="card stat">
    <div class="num"><?= $pending ?></div>
    <div class="label"><a href="users.php?status=pending">pending caller<?= $pending === 1 ? '' : 's' ?></a></div>
  </div>
  <div class="card stat">
    <div class="num"><?= count($online) ?></div>
    <div class="label">on the wire now</div>
  </div>
  <div class="card stat">
    <div class="num"><?= $callsToday ?></div>
    <div class="label">calls today</div>
  </div>
  <div class="card stat">
    <div class="num"><?= $postsToday ?></div>
    <div class="label">posts today</div>
  </div>
  <div class="card stat">
    <?php if ($stale === null): ?>
      <div class="num warn">--</div><div class="label">cron never ran</div>
    <?php elseif ($stale > Cron::HEALTH_RED): ?>
      <div class="num bad"><?= floor($stale / 60) ?>m</div><div class="label">since last cron tick</div>
    <?php else: ?>
      <div class="num ok"><?= $stale < 120 ? $stale . 's' : floor($stale / 60) . 'm' ?></div><div class="label">since last cron tick</div>
    <?php endif; ?>
  </div>
</div>

<?php if ($stale === null || $stale > Cron::HEALTH_RED): ?>
<div class="card">
  <h2 class="bad">Cron looks stale</h2>
  <p>Ticks are more than <?= (int)(Cron::HEALTH_RED / 60) ?> minutes apart. The pseudo-cron fallback
  keeps the board correct whenever callers hit the wire, but for promptness paste this into
  cPanel → Cron Jobs:</p>
  <pre class="cronline"><?= h($cronLine) ?></pre>
  <p class="hint">Web-cron hosts can hit <code><?= h(rtrim((string)$settings->get('base_url', ''), '/')) ?>/cron.php?key=…</code> instead.</p>
</div>
<?php endif; ?>

<div class="cols">
<div>
  <h2 id="chat">The wire <span class="hint">everything on chat_msgs, pages included</span></h2>
  <div class="card">
    <form method="post" class="row">
      <?= csrf_field() ?>
      <input type="hidden" name="action" value="avail">
      <input type="hidden" name="on" value="<?= $available ? '0' : '1' ?>">
      <span>Callers who page are told you are <strong class="<?= $available ? 'ok' : 'warn' ?>"><?= $available ? 'around' : 'away' ?></strong>.</span>
      <button class="btn small secondary"><?= $available ? 'Mark away' : 'Mark available' ?></button>
    </form>
  </div>
  <div class="card">
    <h2 style="margin-top:0">Broadcast <span class="hint">a banner above every caller's screen, then it fades</span></h2>
    <form method="post" class="row">
      <?= csrf_field() ?>
      <input type="hidden" name="action" value="bcast">
      <input type="text" name="text" maxlength="100" placeholder="Board maintenance at nine…" autocomplete="off">
      <select name="minutes">
        <option value="1">1 min</option>
        <option value="2" selected>2 min</option>
        <option value="5">5 min</option>
        <option value="10">10 min</option>
      </select>
      <button class="btn small">Broadcast</button>
    </form>
    <?php $bu = (int)($settings->get('broadcast_until', '0') ?? 0); if ($bu > time()): ?>
    <p class="hint">Live now (<?= (int)ceil(($bu - time()) / 60) ?> min left):
      “<?= h((string)$settings->get('broadcast_msg', '')) ?>”</p>
    <?php endif; ?>
  </div>

  <div class="card chatpane">
    <div id="chatlog" data-max="<?= $chatMax ?>">
      <?php if ($chatRows === []): ?><div class="hint">Nothing on the wire yet.</div><?php endif; ?>
      <?php foreach ($chatRows as $r): ?>
        <div class="chatline <?= h((string)$r['kind']) ?>" data-id="<?= (int)$r['id'] ?>"><?= h(admin_chat_line($r)) ?><?php
            if ($r['to_node'] === null && in_array((string)$r['kind'], ['chat', 'broadcast'], true)): ?> <a href="#chat" class="delchat" title="Delete this line from the public room">&#10005;</a><?php endif; ?></div>
      <?php endforeach; ?>
    </div>
    <div id="privline" class="privline<?= $talkNode > 0 ? '' : ' off' ?>">
      PRIVATE LINE → node <span id="privnode"><?= $talkNode ?></span>
      (<span id="privhandle"><?= h($handleByNode[$talkNode] ?? '?') ?></span>):
      what you send breaks into their screen; only the two of you see it.
      <form method="post" class="inline"><?= csrf_field() ?><input type="hidden" name="action" value="talkto"><input type="hidden" name="node" value="0"><button class="btn small secondary">Back to everyone</button></form>
    </div>
    <form method="post" id="sayform" class="row">
      <?= csrf_field() ?>
      <input type="hidden" name="action" value="say">
      <select name="node" id="nodesel">
        <option value="0">everyone (public chat room)</option>
        <?php foreach ($online as $s): if ($s['handle'] === null) continue; ?>
        <option value="<?= (int)$s['node'] ?>"<?= (int)$s['node'] === $talkNode ? ' selected' : '' ?>><?= h('PRIVATE to node ' . $s['node'] . ' - ' . $s['handle']) ?></option>
        <?php endforeach; ?>
      </select>
      <input type="text" name="body" maxlength="70" placeholder="say something on the wire…" autocomplete="off"<?= $talkNode > 0 ? ' autofocus' : '' ?>>
      <button class="btn small">Send</button>
    </form>
    <p class="hint">"Everyone" drops your line in the public Chat room. Pick a node (or hit a
    Break in button) and it becomes a private SysOp line that breaks into that caller's screen,
    whatever they are doing: the classic break-in chat. They answer from the same screen; only
    you and they see it.</p>
  </div>

  <h2>Who's online</h2>
  <div class="card">
    <table class="list">
      <tr><th>Node</th><th>Handle</th><th>Doing</th><th>On since</th><th></th></tr>
      <?php if ($online === []): ?><tr><td colspan="5" class="hint">The lines are quiet.</td></tr><?php endif; ?>
      <?php foreach ($online as $s): ?>
      <tr>
        <td><?= (int)$s['node'] ?></td>
        <td><?= h($s['handle'] ?? '(logging in)') ?></td>
        <td><?= h(ScreenEngine::describeState((string)$s['state'])) ?></td>
        <td><?= h(admin_stamp((string)$s['created_at'], 'H:i')) ?></td>
        <td class="nowrap"><?php if ($s['handle'] !== null): ?>
          <form method="post" class="inline"><?= csrf_field() ?>
            <input type="hidden" name="action" value="talkto"><input type="hidden" name="node" value="<?= (int)$s['node'] ?>">
            <button class="btn small secondary">Break in</button></form>
        <?php endif; ?>
          <form method="post" class="inline" onsubmit="return confirm('Hang up node <?= (int)$s['node'] ?>?')"><?= csrf_field() ?>
            <input type="hidden" name="action" value="kick"><input type="hidden" name="node" value="<?= (int)$s['node'] ?>">
            <button class="btn small danger">Log off</button></form>
        </td>
      </tr>
      <?php endforeach; ?>
    </table>
  </div>
</div>

<div>
  <h2>Recent events</h2>
  <div class="card">
    <table class="list">
      <?php foreach ($events as $e): ?>
      <tr>
        <td class="hint nowrap"><?= h(admin_stamp((string)$e['at'], 'm-d H:i')) ?></td>
        <td><span class="chip <?= h((string)$e['level']) ?>"><?= h((string)$e['level']) ?></span></td>
        <td><?= h((string)$e['msg']) ?></td>
      </tr>
      <?php endforeach; ?>
    </table>
    <p class="hint"><a href="logs.php">All logs →</a></p>
  </div>
</div>
</div>

<script>
(function () {
  var log = document.getElementById('chatlog');
  var say = document.getElementById('sayform');
  if (!log) return;
  var after = parseInt(log.getAttribute('data-max') || '0', 10);
  function add(rows) {
    rows.forEach(function (r) {
      after = Math.max(after, r.id);
      var div = document.createElement('div');
      div.className = 'chatline ' + r.kind;
      div.setAttribute('data-id', r.id);
      div.textContent = r.line;
      if (r.del) {
        var x = document.createElement('a');
        x.href = '#chat';
        x.className = 'delchat';
        x.title = 'Delete this line from the public room';
        x.textContent = '✕';
        div.appendChild(document.createTextNode(' '));
        div.appendChild(x);
      }
      log.appendChild(div);
    });
    if (rows.length) log.scrollTop = log.scrollHeight;
  }
  // Moderation (1.6.5): the x on a public line deletes that row.
  log.addEventListener('click', function (ev) {
    var t = ev.target;
    if (!t || !t.classList || !t.classList.contains('delchat')) return;
    ev.preventDefault();
    if (!confirm('Delete this line from the public room?')) return;
    var line = t.closest('.chatline');
    var data = new FormData();
    data.append('csrf', <?= json_encode(admin_csrf_token()) ?>);
    data.append('action', 'delchat');
    data.append('id', line ? line.getAttribute('data-id') : '0');
    data.append('ajax', '1');
    fetch('index.php', {method: 'POST', body: data, credentials: 'same-origin'})
      .then(function (r) { return r.json(); })
      .then(function (j) { if (j && j.ok && line) { line.remove(); } })
      .catch(function () {});
  });
  function poll() {
    fetch('index.php?ajax=chat&after=' + after, {credentials: 'same-origin'})
      .then(function (r) { return r.json(); })
      .then(function (j) { add(j.rows || []); })
      .catch(function () {});
  }
  setInterval(poll, 4000);
  // Coming back to the tab polls NOW: browsers throttle background-tab
  // timers, and each poll doubles as the "sysop is watching" heartbeat
  // that callers on the private line listen for (1.5.5).
  document.addEventListener('visibilitychange', function () {
    if (!document.hidden) { poll(); }
  });
  log.scrollTop = log.scrollHeight;
  if (say) say.addEventListener('submit', function (ev) {
    ev.preventDefault();
    var data = new FormData(say);
    data.append('ajax', '1');
    fetch('index.php', {method: 'POST', body: data, credentials: 'same-origin'})
      .then(function (r) { return r.json(); })
      .then(function (j) {
        if (j && j.ok === false) {
          var div = document.createElement('div');
          div.className = 'chatline page';
          div.textContent = '[!] ' + (j.err || 'That did not go out.');
          log.appendChild(div);
          log.scrollTop = log.scrollHeight;
          if (nodesel) { nodesel.value = '0'; }
          syncPriv(0);
          return;
        }
        say.querySelector('[name=body]').value = '';
        poll();
      })
      .catch(function () { say.submit(); });
  });
  // The banner mirrors the select, so the aim is visible BEFORE sending.
  // Aiming itself is server-side (talkto POSTs); this is cosmetics only.
  var nodesel = document.getElementById('nodesel');
  function syncPriv(n) {
    var box = document.getElementById('privline');
    if (!box) { return; }
    if (n > 0) {
      box.classList.remove('off');
      document.getElementById('privnode').textContent = n;
      var opt = nodesel && nodesel.options[nodesel.selectedIndex];
      var m = opt ? opt.textContent.match(/ - (.*)$/) : null;
      document.getElementById('privhandle').textContent = m ? m[1] : '?';
    } else {
      box.classList.add('off');
    }
  }
  if (nodesel) nodesel.addEventListener('change', function () { syncPriv(parseInt(nodesel.value, 10) || 0); });
  // Landing pre-aimed (a Break in / Answer click): cursor in the box.
  if (parseInt(nodesel && nodesel.value || '0', 10) > 0) {
    var bodyIn = say && say.querySelector('[name=body]');
    if (bodyIn) { bodyIn.focus(); }
  }
})();
</script>

<?php admin_page_end(); ?>
