fleet-router 0.6.0

Reverse proxy and load balancer for the Antelope SHiP (State History Plugin) WebSocket protocol.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>fleet-router dashboard</title>
  <meta name="color-scheme" content="dark light">
  <meta name="theme-color" content="#050505" media="(prefers-color-scheme: dark)">
  <meta name="theme-color" content="#f6f6f3" media="(prefers-color-scheme: light)">
  <meta name="robots" content="noindex">
  <link rel="icon" href="favicon.svg" type="image/svg+xml">
  <link rel="preload" href="fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="preload" href="fonts/jetbrains-mono.woff2" as="font" type="font/woff2" crossorigin>
  <script src="js/theme-boot.js"></script>
  <link rel="stylesheet" href="app.css">
  <script type="module" src="js/main.js"></script>
</head>
<body>
  <a class="skip" href="#main">Skip to content</a>

  <header class="masthead">
    <div class="wrap masthead-row">
      <a class="brand" href="#fleet" aria-label="fleet-router, fleet view">
        <svg class="brand-mark" width="24" height="24" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
          <rect x="0.5" y="0.5" width="31" height="31" rx="4" class="brand-mark-field"/>
          <path d="M4 11h8l5 5h11M4 21h8l5-5" fill="none" stroke-width="2" stroke-linejoin="round" class="brand-mark-dim"/>
          <path d="M4 11h8l5 5h11" fill="none" stroke-width="2.5" stroke-linejoin="round" class="brand-mark-set"/>
          <circle cx="26" cy="21" r="2.2" class="brand-mark-lamp"/>
        </svg>
        <span class="brand-name">fleet-router</span>
      </a>

      <p class="mast-meta" aria-live="off">
        <span class="link-state" id="link-state" data-state="connecting">
          <span class="lamp" aria-hidden="true"></span>
          <span class="link-label">Connecting</span>
        </span>
        <span class="meta-item" id="meta-version"></span>
        <span class="meta-item" id="meta-uptime"></span>
      </p>

      <nav class="views" aria-label="Views">
        <a href="#fleet" data-view="fleet">Fleet</a>
        <a href="#sessions" data-view="sessions">Sessions <span class="count" id="nav-sessions"></span></a>
        <a href="#events" data-view="events">Events <span class="count" id="nav-events" hidden></span></a>
      </nav>

      <div class="mast-actions">
        <button class="button button-quiet" id="control-button" type="button" hidden>
          <span class="key" aria-hidden="true"></span>
          <span class="control-label">Controls</span>
        </button>
        <button class="theme-toggle" id="theme-toggle" type="button" aria-label="Switch theme">
          <svg class="icon-sun" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
          <svg class="icon-moon" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z"/></svg>
        </button>
      </div>
    </div>
  </header>

  <main id="main" class="wrap" tabindex="-1">
    <section class="view" id="view-fleet" data-view="fleet" aria-labelledby="fleet-title">
      <div class="view-head">
        <h1 id="fleet-title">Fleet</h1>
        <p class="view-summary" id="fleet-summary"></p>
        <ul class="legend" aria-label="Upstream states">
          <li><span class="key key-set" aria-hidden="true"></span>up</li>
          <li><span class="key key-failover" aria-hidden="true"></span>failover</li>
          <li><span class="key key-stale" aria-hidden="true"></span>stale</li>
          <li><span class="key key-quarantine" aria-hidden="true"></span>quarantined</li>
          <li><span class="key key-offline" aria-hidden="true"></span>down</li>
          <li><span class="key key-idle" aria-hidden="true"></span>disabled</li>
          <li><span class="key-line" aria-hidden="true"></span>finality data</li>
        </ul>
      </div>
      <div id="listeners" class="listeners">
        <p class="placeholder">Waiting for the first status update.</p>
      </div>
      <section class="recent" aria-labelledby="recent-title">
        <div class="recent-head">
          <h2 id="recent-title">Recent events</h2>
          <a href="#events">All events</a>
        </div>
        <ol class="event-list" id="recent-events"></ol>
      </section>
    </section>

    <section class="view" id="view-sessions" data-view="sessions" aria-labelledby="sessions-title" hidden>
      <div class="view-head">
        <h1 id="sessions-title">Sessions</h1>
        <p class="view-summary" id="sessions-summary"></p>
      </div>
      <div class="toolbar">
        <label class="field">
          <span>Listener</span>
          <select id="sessions-listener"><option value="">All</option></select>
        </label>
        <label class="field">
          <span>Upstream</span>
          <select id="sessions-upstream"><option value="">All</option></select>
        </label>
        <label class="field field-grow">
          <span>Client</span>
          <input id="sessions-search" type="search" placeholder="Filter by address" autocomplete="off" spellcheck="false">
        </label>
      </div>
      <div class="table-scroll">
        <table class="data" id="sessions-table">
          <thead>
            <tr>
              <th scope="col" data-sort="client">Client</th>
              <th scope="col" data-sort="upstream_name">Upstream</th>
              <th scope="col" data-sort="age" class="num">Age</th>
              <th scope="col" data-sort="start_block" class="num">Start block</th>
              <th scope="col" data-sort="last_block" class="num">Last block</th>
              <th scope="col" data-sort="blocks" class="num">Blocks</th>
              <th scope="col" data-sort="bytes" class="num">Sent</th>
              <th scope="col" data-sort="failovers" class="num">Failovers</th>
              <th scope="col" class="col-actions"><span class="sr-only">Actions</span></th>
            </tr>
          </thead>
          <tbody></tbody>
        </table>
      </div>
      <p class="empty" id="sessions-empty" hidden></p>
    </section>

    <section class="view" id="view-events" data-view="events" aria-labelledby="events-title" hidden>
      <div class="view-head">
        <h1 id="events-title">Events</h1>
        <p class="view-summary">The last 500 events the router kept in memory, newest first.</p>
      </div>
      <div class="toolbar">
        <div class="segmented" role="radiogroup" aria-label="Level" id="events-level">
          <button type="button" role="radio" aria-checked="true" data-level="">All</button>
          <button type="button" role="radio" aria-checked="false" data-level="warn">Warnings</button>
          <button type="button" role="radio" aria-checked="false" data-level="error">Errors</button>
        </div>
        <label class="field">
          <span>Listener</span>
          <select id="events-listener"><option value="">All</option></select>
        </label>
      </div>
      <ol class="event-list event-list-full" id="all-events"></ol>
      <p class="empty" id="events-empty" hidden>No events match these filters.</p>
    </section>
  </main>

  <dialog class="sheet" id="upstream-dialog" aria-labelledby="upstream-dialog-title">
    <form method="dialog" class="sheet-head">
      <h2 id="upstream-dialog-title"></h2>
      <button class="icon-button" value="close" aria-label="Close">
        <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 3l10 10M13 3L3 13"/></svg>
      </button>
    </form>
    <div class="sheet-body" id="upstream-dialog-body"></div>
  </dialog>

  <dialog class="sheet sheet-narrow" id="unlock-dialog" aria-labelledby="unlock-title">
    <form class="sheet-form" id="unlock-form">
      <div class="sheet-head">
        <h2 id="unlock-title">Unlock controls</h2>
        <button class="icon-button" type="button" data-close aria-label="Close">
          <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 3l10 10M13 3L3 13"/></svg>
        </button>
      </div>
      <div class="sheet-body">
        <p>Enter the control token from this router's config. It stays in this browser tab only and is sent with each control request.</p>
        <label class="field">
          <span>Control token</span>
          <input id="unlock-token" type="password" autocomplete="off" spellcheck="false" required>
        </label>
        <p class="form-error" id="unlock-error" role="alert" hidden></p>
        <div class="sheet-actions">
          <button class="button" type="button" data-close>Cancel</button>
          <button class="button button-primary" type="submit">Unlock</button>
        </div>
      </div>
    </form>
  </dialog>

  <dialog class="sheet sheet-narrow" id="info-dialog" aria-labelledby="info-title">
    <form method="dialog">
      <div class="sheet-head">
        <h2 id="info-title"></h2>
        <button class="icon-button" value="close" aria-label="Close">
          <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 3l10 10M13 3L3 13"/></svg>
        </button>
      </div>
      <div class="sheet-body" id="info-body"></div>
    </form>
  </dialog>

  <dialog class="sheet sheet-narrow" id="confirm-dialog" aria-labelledby="confirm-title">
    <form method="dialog">
      <div class="sheet-head">
        <h2 id="confirm-title"></h2>
      </div>
      <div class="sheet-body">
        <p id="confirm-text"></p>
        <div class="sheet-actions">
          <button class="button" value="cancel">Cancel</button>
          <button class="button button-warn" value="ok" id="confirm-ok"></button>
        </div>
      </div>
    </form>
  </dialog>

  <div class="toasts" id="toasts" role="status" aria-live="polite" popover="manual"></div>
  <div class="sr-only" id="announcer" aria-live="polite"></div>

  <noscript><p class="wrap noscript">The dashboard needs JavaScript. The plain status page at <a href="../status">/status</a> works without it.</p></noscript>
</body>
</html>