mirror of
git://f0xx.org/ac/ac-be-issues
synced 2026-07-29 02:19:14 +03:00
770 lines
43 KiB
PHP
770 lines
43 KiB
PHP
<?php
|
||
/*
|
||
* package examples/crash_reporter/backend/views/layout.php
|
||
* layout.php
|
||
* Created at: Wed 20 May 2026 14:31:55 +0200
|
||
* Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
|
||
* Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
|
||
* Contributors:
|
||
* - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 86 lines)
|
||
* - Cursor Agent (project assistant)
|
||
* Digest: SHA256 21e56cd12df6a94df41a1c0d67be530bd01454865ee1f032cef5f8d2e497509a
|
||
*/
|
||
?>
|
||
<?php
|
||
$__navProjectBase = Auth::projectBasePath();
|
||
$__navIssuesBase = rtrim((string) cfg('links.crashes', $__navProjectBase . '/issues'), '/');
|
||
$__navActive = platform_nav_active_from_view($view ?? null);
|
||
$__navUser = trim((string) (Auth::user()['username'] ?? ''));
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="en" data-theme="dark">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title><?= h($pageTitle ?? 'Console') ?> — <?= h(cfg('app_name')) ?></title>
|
||
<script>
|
||
(function () {
|
||
var t = localStorage.getItem('crash_console_theme');
|
||
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
|
||
var l = localStorage.getItem('crash_console_lang');
|
||
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
|
||
})();
|
||
</script>
|
||
<link rel="stylesheet" href="<?= h(Auth::basePath()) ?>/assets/css/app.css">
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/js/nav_shell.js" defer></script>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/js/i18n.js" defer></script>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/js/app.js" defer></script>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/js/cookie_consent.js" defer></script>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/js/ticket_create.js" defer></script>
|
||
<?php if (in_array($view ?? '', ['tickets', 'ticket'], true)): ?>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/js/tickets.js" defer></script>
|
||
<?php endif; ?>
|
||
<?php if (($view ?? '') === 'graphs'): ?>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/js/graphs.js" defer></script>
|
||
<?php endif; ?>
|
||
<?php if (($view ?? '') === 'live_sessions'): ?>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/js/live_sessions.js" defer></script>
|
||
<?php endif; ?>
|
||
<?php if (($view ?? '') === 'remote_access'): ?>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/js/remote_access.js" defer></script>
|
||
<?php endif; ?>
|
||
<?php if (($view ?? '') === 'short_links'): ?>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/js/short_links.js" defer></script>
|
||
<?php endif; ?>
|
||
<?php if (($view ?? '') === 'rbac'): ?>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/js/rbac_admin.js" defer></script>
|
||
<?php endif; ?>
|
||
<?php if (($view ?? '') === 'media_pipelines'): ?>
|
||
<link rel="stylesheet" href="<?= h(Auth::basePath()) ?>/assets/vendor/nuevo-player/vdjs/v10.0.0/skins/treso/videojs.min.css">
|
||
<style>
|
||
.media-pipelines-player-wrap { max-width: 960px; margin: 0.5rem 0 1rem; background: var(--surface-2, #111); border-radius: 8px; overflow: hidden; }
|
||
.media-pipelines-player-wrap .video-js { width: 100%; }
|
||
.tag-field--wide { flex: 2 1 280px; min-width: 200px; }
|
||
.btn--sm { font-size: 0.8rem; padding: 0.2rem 0.5rem; margin-right: 0.25rem; }
|
||
</style>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/vendor/nuevo-player/js/video.min.js" defer></script>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/vendor/nuevo-player/js/videojs-contrib-hls.min.js" defer></script>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/vendor/nuevo-player/js/nuevo.min.js" defer></script>
|
||
<script src="<?= h(Auth::basePath()) ?>/assets/js/media_pipelines.js" defer></script>
|
||
<?php endif; ?>
|
||
<?php AnalyticsHead::render('crashes'); ?>
|
||
</head>
|
||
<body data-base-path="<?= h(console_base_path($view ?? null)) ?>"
|
||
data-view="<?= h($view ?? 'home') ?>"
|
||
data-can-tag-edit="<?= Auth::canEditTags() ? '1' : '0' ?>"
|
||
<?= (($view ?? '') === 'report' && !empty($report['id'])) ? ' data-report-id="' . (int) $report['id'] . '"' : '' ?>
|
||
<?= (($view ?? '') === 'ticket' && !empty($ticket['id'])) ? ' data-ticket-id="' . (int) $ticket['id'] . '"' : '' ?>
|
||
<?= Rbac::can('remote_access_operate') ? ' data-can-ra-operate="1"' : '' ?>
|
||
<?= Rbac::can('remote_access_admin') ? ' data-can-ra-admin="1"' : '' ?>
|
||
<?= Rbac::can('short_links_operate') ? ' data-can-sl-operate="1"' : '' ?>
|
||
<?= Rbac::isGlobalAdmin() ? ' data-can-sl-admin="1"' : '' ?>
|
||
<?= Rbac::isRoot() ? ' data-can-rbac-root="1"' : '' ?>
|
||
<?= (cfg('media_pipelines.enabled', false) && (Rbac::isGlobalAdmin() || Rbac::can('media_pipelines_operate'))) ? ' data-can-mp-operate="1"' : '' ?>>
|
||
<header class="top-menu" hidden aria-hidden="true"></header>
|
||
<div class="shell<?= ($view ?? '') === 'graphs' ? ' shell--graphs-full' : '' ?>">
|
||
<?php platform_render_nav_shell([
|
||
'project_base' => $__navProjectBase,
|
||
'issues_base' => $__navIssuesBase,
|
||
'active' => $__navActive,
|
||
'username' => $__navUser,
|
||
]); ?>
|
||
<main class="main-pane">
|
||
<?php if (($view ?? 'home') === 'home'): ?>
|
||
<?php require __DIR__ . '/partials/console_home_landing.php'; ?>
|
||
<?php elseif (($view ?? '') === 'report' && !empty($report)): ?>
|
||
<?php require __DIR__ . '/report_detail.php'; ?>
|
||
<?php elseif (($view ?? '') === 'ticket' && !empty($ticket)): ?>
|
||
<?php require __DIR__ . '/ticket_detail.php'; ?>
|
||
<?php elseif (($view ?? '') === 'tickets'): ?>
|
||
<div id="tickets-app" class="reports-app tickets-app">
|
||
<div class="toolbar reports-toolbar">
|
||
<h1 data-i18n="tickets.title">Tickets</h1>
|
||
<div class="toolbar-actions">
|
||
<button type="button" class="btn btn-primary js-new-ticket-btn" data-i18n="tickets.new">New ticket</button>
|
||
<label class="toolbar-select">
|
||
<span data-i18n="theme.label">Theme</span>
|
||
<select id="theme-select" aria-label="UI theme">
|
||
<option value="dark">Dark</option>
|
||
<option value="light">Light</option>
|
||
</select>
|
||
</label>
|
||
<label class="toolbar-select">
|
||
<span data-i18n="reports.per_page">Per page</span>
|
||
<select id="tickets-per-page" aria-label="Tickets per page">
|
||
<option value="25">25</option>
|
||
<option value="50" selected>50</option>
|
||
<option value="75">75</option>
|
||
<option value="100">100</option>
|
||
</select>
|
||
</label>
|
||
<label class="toolbar-select">
|
||
<span data-i18n="tickets.filter_tag">Status tag</span>
|
||
<select id="tickets-tag-filter" aria-label="Filter by tag">
|
||
<option value="" data-i18n="tickets.filter_all">All</option>
|
||
<option value="open">open</option>
|
||
<option value="in-progress">in progress</option>
|
||
<option value="confirmed">confirmed</option>
|
||
<option value="closed">closed</option>
|
||
<option value="urgent">urgent</option>
|
||
<option value="20260604">20260604 (roadmap)</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<p id="tickets-status" class="reports-status muted" aria-live="polite" data-i18n="reports.loading">Loading…</p>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-tree reports-table--cols" id="tickets-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="report-tree-head" aria-label="Expand"></th>
|
||
<th class="sortable" data-sort="title" scope="col" data-i18n="tickets.col_issue">Issue</th>
|
||
<th class="sortable sortable--active" data-sort="opened_at_ms" scope="col" data-i18n="tickets.col_opened">Opened</th>
|
||
<th scope="col" data-i18n="tickets.col_env">App / OS</th>
|
||
<th class="sortable" data-sort="rating" scope="col" data-i18n="col.rating">Rating</th>
|
||
<th class="col-tags" scope="col" data-i18n="col.tags">Tags</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tickets-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
<nav class="reports-pagination" id="tickets-pagination" aria-label="Tickets pages"></nav>
|
||
</div>
|
||
<?php elseif (($view ?? '') === 'reports'): ?>
|
||
<div id="reports-app" class="reports-app" data-grouped="0">
|
||
<div class="toolbar reports-toolbar">
|
||
<h1 data-i18n="reports.title">Issues</h1>
|
||
<div class="toolbar-actions">
|
||
<button type="button" class="btn btn-primary js-new-issue-btn" data-i18n="issues.new">New issue</button>
|
||
<button type="button" class="btn reports-mode-btn active" data-grouped="0" data-i18n="reports.by_time">By time</button>
|
||
<button type="button" class="btn reports-mode-btn" data-grouped="1" data-i18n="reports.grouped">Grouped</button>
|
||
<label class="toolbar-select">
|
||
<span data-i18n="theme.label">Theme</span>
|
||
<select id="theme-select" aria-label="UI theme" data-i18n-options="theme">
|
||
<option value="dark" data-i18n="theme.dark">Dark</option>
|
||
<option value="light" data-i18n="theme.light">Light</option>
|
||
</select>
|
||
</label>
|
||
<label class="toolbar-select tag-mode-select-wrap" id="tag-mode-wrap" hidden>
|
||
<span data-i18n="tag.filter_mode">Match</span>
|
||
<select id="tag-mode-select" aria-label="Tag filter mode">
|
||
<option value="and" data-i18n="tag.filter_and">All tags (AND)</option>
|
||
<option value="or" data-i18n="tag.filter_or">Any tag (OR)</option>
|
||
</select>
|
||
</label>
|
||
<label class="toolbar-select">
|
||
<span data-i18n="reports.per_page">Per page</span>
|
||
<select id="per-page-select" aria-label="Reports per page">
|
||
<option value="25">25</option>
|
||
<option value="50" selected>50</option>
|
||
<option value="75">75</option>
|
||
<option value="100">100</option>
|
||
<option value="200">200</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="tag-filter-bar" id="tag-filter-bar">
|
||
<span class="muted tag-filter-label" data-i18n="tag.filter_label">Filter by tag</span>
|
||
<div class="tag-filter-chips" id="tag-filter-chips" role="group" aria-label="Tag filters"></div>
|
||
</div>
|
||
<p id="reports-filter-banner" class="reports-filter-banner muted" hidden></p>
|
||
<div class="reports-search-row">
|
||
<label class="reports-search-label" for="reports-search-input" data-i18n="reports.search">Search</label>
|
||
<div class="reports-search-field">
|
||
<input type="search" id="reports-search-input" class="reports-search-input"
|
||
data-i18n-placeholder="reports.search_placeholder"
|
||
placeholder="Keywords (exceptions, devices, stack traces…)" autocomplete="off" spellcheck="false">
|
||
<ul id="reports-search-suggest" class="reports-search-suggest" hidden role="listbox"></ul>
|
||
</div>
|
||
<button type="button" class="btn" id="reports-search-clear" hidden data-i18n="reports.clear">Clear</button>
|
||
</div>
|
||
<p id="reports-status" class="reports-status muted" aria-live="polite" data-i18n="reports.loading">Loading…</p>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-tree reports-table--cols" id="reports-tree">
|
||
<colgroup id="reports-colgroup"></colgroup>
|
||
<thead id="reports-thead"></thead>
|
||
<tbody id="reports-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
<nav class="reports-pagination" id="reports-pagination" aria-label="Reports pages"></nav>
|
||
</div>
|
||
<?php elseif (($view ?? '') === 'graphs'): ?>
|
||
<div id="graphs-app" class="reports-app graphs-app">
|
||
<div class="toolbar reports-toolbar">
|
||
<h1>AndroidCast analytics</h1>
|
||
<div class="toolbar-actions">
|
||
<label class="toolbar-select">
|
||
<span>Theme</span>
|
||
<select id="theme-select" aria-label="UI theme">
|
||
<option value="dark">Dark</option>
|
||
<option value="light">Light</option>
|
||
</select>
|
||
</label>
|
||
<label class="toolbar-select">
|
||
<span>Window</span>
|
||
<select id="graphs-days" aria-label="Graphs window">
|
||
<option value="1">1d</option>
|
||
<option value="7">7d</option>
|
||
<option value="14" selected>14d</option>
|
||
<option value="30">30d</option>
|
||
</select>
|
||
</label>
|
||
<label class="toolbar-select">
|
||
<span>Columns</span>
|
||
<select id="graphs-columns" aria-label="Graph grid columns">
|
||
<option value="1">1</option>
|
||
<option value="2" selected>2</option>
|
||
<option value="4">4</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<?php require __DIR__ . '/partials/console_quick_links.php'; ?>
|
||
<p id="graphs-session-notice" class="graphs-session-notice" role="status" hidden data-i18n="graphs.session_deficit">
|
||
Session charts are empty. On the device: Developer settings → enable “Grab session stats”, cast a session, then wait for upload to graph_upload.php.
|
||
</p>
|
||
<p id="graphs-status" class="reports-status muted" aria-live="polite">Loading…</p>
|
||
|
||
<section class="graphs-scope" data-graph-scope="user">
|
||
<h2 class="graphs-scope-title">Your activity</h2>
|
||
<div class="cards cards--lift graphs-grid">
|
||
<article class="card card--lift"><h3>Sessions / day</h3><canvas id="graph-user-sessions" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Unique devices / day</h3><canvas id="graph-user-devices" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Avg duration (s) / day</h3><canvas id="graph-user-duration" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Send sessions / day</h3><canvas id="graph-user-send" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Receive sessions / day</h3><canvas id="graph-user-recv" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Success ratio</h3><p id="graph-user-success" class="graph-kpi">—</p></article>
|
||
<article class="card card--lift"><h3>Outbound kbps / day</h3><canvas id="graph-user-bitrate" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Inbound kbps / day</h3><canvas id="graph-user-recv-kbps" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Issues / day</h3><canvas id="graph-user-crashes" width="520" height="160"></canvas></article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="graphs-scope" data-graph-scope="slug_admin" hidden>
|
||
<h2 class="graphs-scope-title">Company (slug admin)</h2>
|
||
<div class="cards cards--lift graphs-grid">
|
||
<article class="card card--lift"><h3>Sessions / day</h3><canvas id="graph-slug-sessions" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Unique devices / day</h3><canvas id="graph-slug-devices" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Install sources</h3><canvas id="graph-slug-install-pie" width="520" height="180"></canvas></article>
|
||
<article class="card card--lift"><h3>NTP sources</h3><canvas id="graph-slug-ntp-pie" width="520" height="180"></canvas></article>
|
||
<article class="card card--lift"><h3>Transport mix</h3><div id="graph-slug-transport" class="graph-breakdown"></div></article>
|
||
<article class="card card--lift"><h3>App versions</h3><div id="graph-slug-versions" class="graph-breakdown"></div></article>
|
||
<article class="card card--lift"><h3>Issues / day</h3><canvas id="graph-slug-crashes" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Tickets / day</h3><canvas id="graph-slug-tickets" width="520" height="160"></canvas></article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="graphs-scope" data-graph-scope="platform_admin" hidden>
|
||
<h2 class="graphs-scope-title">Platform reliability</h2>
|
||
<div class="cards cards--lift graphs-grid">
|
||
<article class="card card--lift"><h3>Sessions / day (all slugs)</h3><canvas id="graph-platform-sessions" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Issues / day</h3><canvas id="graph-platform-crashes" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Tickets / day</h3><canvas id="graph-platform-tickets" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Install sources</h3><canvas id="graph-platform-install-pie" width="520" height="180"></canvas></article>
|
||
<article class="card card--lift"><h3>NTP correction</h3><p id="graph-platform-ntp-avg" class="graph-kpi muted">—</p></article>
|
||
<article class="card card--lift"><h3>Top issue fingerprints</h3><div id="graph-platform-fingerprints" class="graph-breakdown"></div></article>
|
||
<article class="card card--lift"><h3>Issues by app version</h3><canvas id="graph-platform-crash-versions" width="520" height="180"></canvas></article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="graphs-scope" id="graphs-live-cast-section">
|
||
<h2 class="graphs-scope-title">Live cast</h2>
|
||
<div class="cards cards--lift graphs-grid">
|
||
<article class="card card--lift"><h3>Live casts / day</h3><canvas id="graph-live-casts" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Avg cast length (s) / day</h3><canvas id="graph-live-avg-length" width="520" height="160"></canvas></article>
|
||
<article class="card card--lift"><h3>Cast platforms</h3><canvas id="graph-live-platform-pie" width="520" height="180"></canvas></article>
|
||
<article class="card card--lift"><h3>Join platforms</h3><canvas id="graph-live-join-platform-pie" width="520" height="180"></canvas></article>
|
||
<article class="card card--lift"><h3>Video codecs</h3><canvas id="graph-live-video-codec-pie" width="520" height="180"></canvas></article>
|
||
<article class="card card--lift"><h3>Audio codecs</h3><canvas id="graph-live-audio-codec-pie" width="520" height="180"></canvas></article>
|
||
<article class="card card--lift"><h3>Bandwidth modes</h3><canvas id="graph-live-bw-pie" width="520" height="180"></canvas></article>
|
||
<article class="card card--lift"><h3>Top casters</h3><div id="graph-live-top-users" class="graph-breakdown"></div></article>
|
||
</div>
|
||
</section>
|
||
|
||
<div id="graph-detail-overlay" class="graph-detail-overlay" hidden>
|
||
<div class="graph-detail-panel" role="dialog" aria-modal="true" aria-labelledby="graph-detail-title">
|
||
<header class="graph-detail-header">
|
||
<div class="graph-detail-header-text">
|
||
<h2 id="graph-detail-title">Graph</h2>
|
||
<p id="graph-detail-stats" class="graph-detail-stats muted" aria-live="polite"></p>
|
||
</div>
|
||
<button type="button" class="btn graph-detail-close" id="graph-detail-close" aria-label="Close full view">Close</button>
|
||
<button type="button" class="btn" id="graph-detail-copy-link">Copy link</button>
|
||
<a class="btn" id="graph-detail-share" href="#" hidden aria-hidden="true"></a>
|
||
</header>
|
||
<div class="graph-detail-canvas-wrap">
|
||
<canvas id="graph-detail-canvas" hidden></canvas>
|
||
<div id="graph-detail-body" class="graph-detail-body"></div>
|
||
<p id="graph-detail-tooltip" class="graph-detail-tooltip" hidden></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php elseif (($view ?? '') === 'live_sessions'): ?>
|
||
<div id="live-sessions-app" class="reports-app">
|
||
<div class="toolbar reports-toolbar">
|
||
<h1>Live cast sessions</h1>
|
||
<div class="toolbar-actions">
|
||
<label class="toolbar-select">
|
||
<span>Window</span>
|
||
<select id="live-sessions-days" aria-label="Sessions window">
|
||
<option value="1">1d</option>
|
||
<option value="7">7d</option>
|
||
<option value="14" selected>14d</option>
|
||
<option value="30">30d</option>
|
||
</select>
|
||
</label>
|
||
<a class="btn" href="<?= h(Auth::basePath()) ?>/live/education">Education demo</a>
|
||
</div>
|
||
</div>
|
||
<?php require __DIR__ . '/partials/console_quick_links.php'; ?>
|
||
<p id="live-sessions-status" class="reports-status muted" aria-live="polite">Loading…</p>
|
||
<p class="muted graphs-footnote">Read-only session tree for all signed-in users. Sorted by last heartbeat (most recent first).</p>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-table--cols" id="live-sessions-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Status</th>
|
||
<th>User</th>
|
||
<th>Platform</th>
|
||
<th>Video</th>
|
||
<th>Duration</th>
|
||
<th>Last heartbeat</th>
|
||
<th>Join opens</th>
|
||
<th>Link</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="live-sessions-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<?php elseif (($view ?? '') === 'remote_access'): ?>
|
||
<div id="remote-access-app" class="reports-app">
|
||
<div class="toolbar reports-toolbar">
|
||
<h1>Remote access</h1>
|
||
<div class="toolbar-actions">
|
||
<label class="toolbar-select">
|
||
<span>Theme</span>
|
||
<select id="theme-select" aria-label="UI theme">
|
||
<option value="dark">Dark</option>
|
||
<option value="light">Light</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<?php require __DIR__ . '/partials/console_quick_links.php'; ?>
|
||
<p id="ra-status" class="reports-status muted" aria-live="polite">Loading…</p>
|
||
|
||
<section class="graphs-scope">
|
||
<h2 class="graphs-scope-title">Devices</h2>
|
||
<p class="muted graphs-footnote ra-whitelist-hint">
|
||
Devices appear after they poll remote access (dev settings → WireGuard or RSSH). Sort is by <strong>last seen</strong> — top rows are actively polling.
|
||
Whitelist rows marked <span class="tag-pill tag-pill--warn">Needs whitelist</span> (opted in, not yet allowed).
|
||
Stale rows have not polled in 7+ days.
|
||
<strong>Poll source IP</strong> is the address BE sees on the HTTP request (often a router or proxy on <code>10.7.x.x</code>) — not the device WAN or LAN.
|
||
</p>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-tree reports-table--cols" id="ra-devices-table">
|
||
<colgroup id="ra-devices-colgroup"></colgroup>
|
||
<thead id="ra-devices-thead"></thead>
|
||
<tbody id="ra-devices-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
<form id="ra-whitelist-form" class="tag-editor-add" style="margin-top:1rem">
|
||
<label class="tag-field"><span>Device ID</span><input type="text" name="device_id" required maxlength="128"></label>
|
||
<label class="tag-field"><span>Notes</span><input type="text" name="notes" maxlength="255"></label>
|
||
<button type="submit" class="btn">Add / whitelist</button>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="graphs-scope">
|
||
<h2 class="graphs-scope-title">Active / pending sessions</h2>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-table--cols" id="ra-active-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">Session</th>
|
||
<th scope="col">Device</th>
|
||
<th scope="col">Status</th>
|
||
<th scope="col">Tunnel</th>
|
||
<th scope="col">Endpoint</th>
|
||
<th scope="col">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="ra-active-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="graphs-scope">
|
||
<h2 class="graphs-scope-title">Inactive / closed sessions</h2>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-table--cols" id="ra-inactive-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">Session</th>
|
||
<th scope="col">Device</th>
|
||
<th scope="col">Status</th>
|
||
<th scope="col">Closed</th>
|
||
<th scope="col">Reason</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="ra-inactive-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="graphs-scope">
|
||
<h2 class="graphs-scope-title">Audit log</h2>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-table--cols" id="ra-events-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">Time</th>
|
||
<th scope="col">Device</th>
|
||
<th scope="col">Action</th>
|
||
<th scope="col">Reason</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="ra-events-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
<?php elseif (($view ?? '') === 'media_pipelines'): ?>
|
||
<div id="media-pipelines-app" class="reports-app">
|
||
<div class="toolbar reports-toolbar">
|
||
<h1>Media pipelines <span class="muted" style="font-size:0.65em;font-weight:normal">(experimental)</span></h1>
|
||
<div class="toolbar-actions">
|
||
<button type="button" class="btn" id="mp-refresh">Refresh</button>
|
||
</div>
|
||
</div>
|
||
<?php require __DIR__ . '/partials/console_quick_links.php'; ?>
|
||
<p id="mp-status" class="reports-status muted" aria-live="polite">Loading…</p>
|
||
<p class="muted graphs-footnote">
|
||
Lab-only restream control plane. Requires <code>streaming_v2</code> on BE, ffmpeg, yt-dlp.
|
||
Visible when <code>media_pipelines.enabled</code> and global admin / privilege grant.
|
||
</p>
|
||
|
||
<section class="graphs-scope" id="mp-create-section">
|
||
<h2 class="graphs-scope-title">New pipeline</h2>
|
||
<form id="mp-create-form" class="tag-editor-add">
|
||
<label class="tag-field"><span>Name</span><input type="text" name="name" maxlength="128" placeholder="My restream"></label>
|
||
<label class="tag-field tag-field--wide"><span>Source URL</span><input type="url" name="source_url" required placeholder="https://… or /path/file.mkv"></label>
|
||
<label class="tag-field"><span>yt-dlp format</span><input type="text" name="source_fmt" placeholder="bv*+ba/b"></label>
|
||
<label class="tag-field tag-field--wide"><span>Separate audio URL</span><input type="url" name="audio_source" placeholder="optional"></label>
|
||
<label class="tag-field">
|
||
<span>Filter</span>
|
||
<select name="filter_profile">
|
||
<option value="default">Logo overlay</option>
|
||
<option value="blurred">Blurred + logo</option>
|
||
<option value="nologo">No logo</option>
|
||
</select>
|
||
</label>
|
||
<label class="tag-field">
|
||
<span>Preset</span>
|
||
<select name="output_preset">
|
||
<option value="1080p">1080p</option>
|
||
<option value="720p">720p</option>
|
||
</select>
|
||
</label>
|
||
<label class="tag-field">
|
||
<span>Mode</span>
|
||
<select name="run_mode">
|
||
<option value="full">Full (encode + RTMP fanout)</option>
|
||
<option value="encode_only">Encode only (record + HLS)</option>
|
||
</select>
|
||
</label>
|
||
<button type="submit" class="btn" id="mp-create-btn">Create pipeline</button>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="graphs-scope">
|
||
<h2 class="graphs-scope-title">Pipelines</h2>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-table--cols" id="mp-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Status</th>
|
||
<th>Mode</th>
|
||
<th>Source</th>
|
||
<th>HLS</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="mp-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="graphs-scope" id="mp-player-section" hidden>
|
||
<h2 class="graphs-scope-title">Preview</h2>
|
||
<div class="media-pipelines-player-wrap">
|
||
<video id="mp-player" class="video-js vjs-default-skin vjs-big-play-centered" controls preload="auto" width="960" height="540"></video>
|
||
</div>
|
||
<p class="muted graphs-footnote" id="mp-player-url"></p>
|
||
</section>
|
||
|
||
<section class="graphs-scope" id="mp-probe-section" hidden>
|
||
<h2 class="graphs-scope-title">Source profile (ffprobe)</h2>
|
||
<pre id="mp-probe-json" class="graph-detail-body" style="max-height:240px;overflow:auto"></pre>
|
||
</section>
|
||
|
||
<section class="graphs-scope" id="mp-logs-section" hidden>
|
||
<h2 class="graphs-scope-title">Encode log (tail)</h2>
|
||
<pre id="mp-logs" class="graph-detail-body" style="max-height:200px;overflow:auto;font-size:0.75rem"></pre>
|
||
</section>
|
||
</div>
|
||
<?php elseif (($view ?? '') === 'short_links'): ?>
|
||
<div id="short-links-app" class="reports-app">
|
||
<div class="toolbar reports-toolbar">
|
||
<h1>Short links</h1>
|
||
<div class="toolbar-actions">
|
||
<label class="toolbar-select">
|
||
<span>Theme</span>
|
||
<select id="theme-select" aria-label="UI theme">
|
||
<option value="dark">Dark</option>
|
||
<option value="light">Light</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<?php require __DIR__ . '/partials/console_quick_links.php'; ?>
|
||
<p id="sl-status" class="reports-status muted" aria-live="polite">Loading…</p>
|
||
<p class="muted graphs-footnote">
|
||
Public API: <code>https://s.f0xx.org/api/v1/shorten</code> · machine clients use bearer tokens minted here.
|
||
</p>
|
||
|
||
<section class="graphs-scope">
|
||
<h2 class="graphs-scope-title">Bearer tokens</h2>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-table--cols" id="sl-bearers-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">ID</th>
|
||
<th scope="col">Label</th>
|
||
<th scope="col">Status</th>
|
||
<th scope="col">Caps</th>
|
||
<th scope="col">Links</th>
|
||
<th scope="col">Rate/h</th>
|
||
<th scope="col">Created</th>
|
||
<th scope="col">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="sl-bearers-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
<form id="sl-mint-form" class="tag-editor-add" style="margin-top:1rem"<?= Rbac::can('short_links_operate') ? '' : ' hidden' ?>>
|
||
<label class="tag-field"><span>Label</span><input type="text" name="label" required maxlength="128" placeholder="prod-hub"></label>
|
||
<label class="tag-field"><span>Rate limit / hour</span><input type="number" name="rate_limit" value="1000" min="1" max="100000"></label>
|
||
<label class="tag-field tag-field--check"><span><input type="checkbox" name="can_temporary" value="1" checked> Temporary links</span></label>
|
||
<label class="tag-field tag-field--check"><span><input type="checkbox" name="can_permanent" value="1" id="sl-can-permanent"> Permanent links</span></label>
|
||
<label class="tag-field tag-field--check" id="sl-mint-signer-wrap" hidden><span><input type="checkbox" name="mint_signer" value="1" checked> Mint signer key</span></label>
|
||
<button type="submit" class="btn btn-primary">Mint bearer</button>
|
||
<?php if (Rbac::isGlobalAdmin()): ?>
|
||
<button type="button" class="btn" id="sl-purge-btn">Purge expired</button>
|
||
<?php endif; ?>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="graphs-scope">
|
||
<h2 class="graphs-scope-title">Signer keys (permanent links)</h2>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-table--cols" id="sl-signers-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">ID</th>
|
||
<th scope="col">Label</th>
|
||
<th scope="col">Status</th>
|
||
<th scope="col">Created</th>
|
||
<th scope="col">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="sl-signers-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
<form id="sl-mint-signer-form" class="tag-editor-add" style="margin-top:1rem"<?= Rbac::can('short_links_operate') ? '' : ' hidden' ?>>
|
||
<label class="tag-field"><span>Label</span><input type="text" name="label" required maxlength="128" placeholder="prod-perm"></label>
|
||
<button type="submit" class="btn">Mint signer</button>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="graphs-scope" id="sl-links-section" hidden>
|
||
<h2 class="graphs-scope-title">Links for bearer <code id="sl-links-bearer-label">—</code></h2>
|
||
<form id="sl-shorten-form" class="tag-editor-add" style="margin-bottom:1rem"<?= Rbac::can('short_links_operate') ? '' : ' hidden' ?>>
|
||
<input type="hidden" name="bearer_id" id="sl-shorten-bearer-id" value="">
|
||
<label class="tag-field tag-field--wide"><span>Long URL</span><input type="url" name="url" required maxlength="2048" placeholder="https://example.com/path"></label>
|
||
<label class="tag-field tag-field--check"><span><input type="checkbox" name="permanent" value="1" id="sl-permanent-cb"> Permanent (ttl=0)</span></label>
|
||
<label class="tag-field" id="sl-ttl-wrap"><span>TTL (seconds)</span><input type="number" name="ttl" id="sl-ttl-input" value="86400" min="60" max="31536000"></label>
|
||
<label class="tag-field tag-field--wide" id="sl-signer-wrap" hidden><span>Signer key</span><input type="password" name="signer" id="sl-signer-input" autocomplete="off" placeholder="required for permanent"></label>
|
||
<button type="submit" class="btn btn-primary">Shorten</button>
|
||
</form>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-table--cols" id="sl-links-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">Short URL</th>
|
||
<th scope="col">Origin</th>
|
||
<th scope="col">Expires</th>
|
||
<th scope="col">Hits</th>
|
||
<th scope="col">QR</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="sl-links-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="graphs-scope">
|
||
<h2 class="graphs-scope-title">Audit log</h2>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-table--cols" id="sl-audit-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">Time</th>
|
||
<th scope="col">Event</th>
|
||
<th scope="col">Bearer</th>
|
||
<th scope="col">Slug</th>
|
||
<th scope="col">Detail</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="sl-audit-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<div id="sl-token-modal" class="graph-detail-overlay" hidden>
|
||
<div class="graph-detail-panel" role="dialog" aria-modal="true" aria-labelledby="sl-token-title">
|
||
<header class="graph-detail-header">
|
||
<h2 id="sl-token-title">Bearer token — copy now</h2>
|
||
<button type="button" class="btn graph-detail-close" id="sl-token-close">Close</button>
|
||
</header>
|
||
<p class="muted">Shown once. Store in a secrets manager or <code>BEARER=…</code> for curl scripts.</p>
|
||
<p><code id="sl-token-value" style="word-break:break-all"></code></p>
|
||
<button type="button" class="btn btn-primary" id="sl-token-copy">Copy token</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="sl-signer-modal" class="graph-detail-overlay" hidden>
|
||
<div class="graph-detail-panel" role="dialog" aria-modal="true" aria-labelledby="sl-signer-title">
|
||
<header class="graph-detail-header">
|
||
<h2 id="sl-signer-title">Signer key — copy now</h2>
|
||
<button type="button" class="btn graph-detail-close" id="sl-signer-close">Close</button>
|
||
</header>
|
||
<p class="muted">Shown once. Required for <code>ttl=0</code> permanent links with a bearer that has permanent capability.</p>
|
||
<p><code id="sl-signer-value" style="word-break:break-all"></code></p>
|
||
<button type="button" class="btn btn-primary" id="sl-signer-copy">Copy signer</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php elseif (($view ?? '') === 'rbac'): ?>
|
||
<div id="rbac-app" class="reports-app">
|
||
<div class="toolbar reports-toolbar">
|
||
<h1>Access control</h1>
|
||
</div>
|
||
<p id="rbac-status" class="reports-status muted" aria-live="polite">Loading…</p>
|
||
<p id="rbac-scope-hint" class="muted graphs-footnote"></p>
|
||
<section class="graphs-scope">
|
||
<h2 class="graphs-scope-title">Users & company roles</h2>
|
||
<div class="reports-table-wrap">
|
||
<table class="data-table reports-table--cols" id="rbac-users-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">User</th>
|
||
<th scope="col">Global role</th>
|
||
<th scope="col">Company</th>
|
||
<th scope="col">Company role</th>
|
||
<th scope="col">Privilege set</th>
|
||
<th scope="col">Auth lockouts</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="rbac-users-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
<p class="muted graphs-footnote">
|
||
API: <code><?= h(Auth::basePath()) ?>/api/rbac.php</code> ·
|
||
Root edits global roles; company owner/admin edits memberships and remote-access privilege sets.
|
||
</p>
|
||
</div>
|
||
<?php endif; ?>
|
||
</main>
|
||
</div>
|
||
<?php if (Auth::canEditTags()): ?>
|
||
<div id="tag-modal" class="tag-modal" hidden aria-hidden="true">
|
||
<div class="tag-modal-backdrop" data-tag-modal-close></div>
|
||
<div class="tag-modal-panel" role="dialog" aria-labelledby="tag-modal-title">
|
||
<header class="tag-modal-header">
|
||
<h2 id="tag-modal-title" data-i18n="tag.edit_title">Edit tags</h2>
|
||
<button type="button" class="tag-modal-close" data-tag-modal-close data-i18n-aria="tag.close" aria-label="Close">×</button>
|
||
</header>
|
||
<div class="tag-editor tag-editor--modal" id="tag-editor-modal" data-report-id="0">
|
||
<div class="tag-editor-preview" id="tag-modal-preview"></div>
|
||
<ul class="tag-editor-list" id="tag-modal-list"></ul>
|
||
<form class="tag-editor-add" id="tag-modal-add">
|
||
<label class="tag-field"><span data-i18n="tag.label">Label</span><input type="text" name="label" maxlength="40" required></label>
|
||
<label class="tag-field"><span data-i18n="tag.color">Color</span><input type="color" name="bg" value="#5c6b82"></label>
|
||
<button type="submit" class="btn" data-i18n="tag.add">Add</button>
|
||
</form>
|
||
<div class="tag-preset-bar" id="tag-modal-presets" hidden>
|
||
<span class="muted" data-i18n="tag.suggestions">Suggestions:</span>
|
||
<div class="tag-preset-chips" id="tag-modal-preset-chips"></div>
|
||
</div>
|
||
<div class="tag-editor-actions">
|
||
<button type="button" class="btn btn-primary" id="tag-modal-save">Save</button>
|
||
<span class="tag-editor-status muted" id="tag-modal-status"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
<dialog id="ticket-create-dialog" class="ticket-create-dialog">
|
||
<form method="dialog" id="ticket-create-form" class="ticket-create-form">
|
||
<header class="ticket-create-header">
|
||
<h2 id="ticket-create-heading" data-i18n="tickets.new">New ticket</h2>
|
||
<button type="button" class="ticket-create-close" id="ticket-create-close" data-i18n-aria="tag.close" aria-label="Close">×</button>
|
||
</header>
|
||
<div class="ticket-create-body">
|
||
<label><span data-i18n="ticket.title">Title</span>
|
||
<input name="title" id="ticket-create-title" required maxlength="256" autofocus></label>
|
||
<label><span data-i18n="ticket.brief">Brief</span>
|
||
<input name="brief" id="ticket-create-brief" maxlength="512"></label>
|
||
<label><span data-i18n="ticket.body">Description</span>
|
||
<textarea name="body" id="ticket-create-body" rows="6"></textarea></label>
|
||
</div>
|
||
<footer class="ticket-create-footer">
|
||
<p id="ticket-create-status" class="muted" aria-live="polite"></p>
|
||
<div class="ticket-create-actions">
|
||
<button type="button" class="btn" id="ticket-create-cancel" data-i18n="ticket.cancel">Cancel</button>
|
||
<button type="submit" class="btn btn-primary" data-i18n="ticket.create">Create</button>
|
||
</div>
|
||
</footer>
|
||
</form>
|
||
</dialog>
|
||
<?php require __DIR__ . '/partials/cookie_consent.php'; ?>
|
||
<?php platform_render_footer(); ?>
|
||
</body>
|
||
</html>
|