mirror of
git://f0xx.org/android_cast
synced 2026-07-29 04:38:53 +03:00
be sync
This commit is contained in:
@@ -15,6 +15,9 @@
|
||||
"nav.remote": "Remote access",
|
||||
"nav.builder": "Builder",
|
||||
"nav.toggle": "Navigation",
|
||||
"nav.security": "Security",
|
||||
"nav.logout": "Logout",
|
||||
"nav.sign_in": "Sign in",
|
||||
"lang.label": "Language",
|
||||
"theme.label": "Theme",
|
||||
"rail.label": "Page navigation",
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"nav.remote": "Удалённый доступ",
|
||||
"nav.builder": "Сборщик",
|
||||
"nav.toggle": "Навигация",
|
||||
"nav.security": "Безопасность",
|
||||
"nav.logout": "Выход",
|
||||
"nav.sign_in": "Вход",
|
||||
"lang.label": "Язык",
|
||||
"theme.label": "Тема",
|
||||
"rail.label": "Навигация по страницам",
|
||||
|
||||
@@ -158,12 +158,26 @@
|
||||
/* (2) Left shell — console nav-pane */
|
||||
.landing-left.nav-pane {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 var(--landing-left-w);
|
||||
width: var(--landing-left-w);
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 3;
|
||||
pointer-events: auto;
|
||||
border-right: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
.landing-left .nav-list {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.landing-left .nav-user {
|
||||
margin-top: auto;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.landing-left.nav-pane.open {
|
||||
width: 200px;
|
||||
|
||||
@@ -4,7 +4,22 @@ if (!is_file($__platformDir . '/footer.php')) {
|
||||
$__platformDir = dirname(__DIR__) . '/platform';
|
||||
}
|
||||
require_once $__platformDir . '/footer.php';
|
||||
unset($__platformDir);
|
||||
$__sessionFile = $__platformDir . '/shared_session.php';
|
||||
$hubCrashesBase = '/app/androidcast_project/crashes';
|
||||
$hubUsername = '';
|
||||
if (is_file($__sessionFile) && function_exists('session_start')) {
|
||||
require_once $__sessionFile;
|
||||
platform_start_session('ac_crash_sess', '/app/androidcast_project');
|
||||
$hubUser = $_SESSION['user'] ?? null;
|
||||
if (is_array($hubUser)) {
|
||||
$hubUsername = trim((string) ($hubUser['username'] ?? ''));
|
||||
}
|
||||
}
|
||||
unset($__platformDir, $__sessionFile);
|
||||
|
||||
function hub_h(string $s): string {
|
||||
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark" data-clock-font="mixed">
|
||||
@@ -129,6 +144,36 @@ unset($__platformDir);
|
||||
<li><a href="crashes/?view=remote_access" class="nav-link"><span class="nav-icon nav-icon--reports"></span><span class="nav-label" data-i18n="nav.remote">Remote access</span></a></li>
|
||||
<li><a href="build/" class="nav-link"><span class="nav-icon nav-icon--builder"></span><span class="nav-label" data-i18n="nav.builder">Builder</span></a></li>
|
||||
</ul>
|
||||
<div class="nav-user">
|
||||
<?php if ($hubUsername !== ''): ?>
|
||||
<span class="nav-user-name" title="<?= hub_h($hubUsername) ?>">
|
||||
<span class="nav-icon nav-icon--user" aria-hidden="true"></span>
|
||||
<span class="nav-label"><?= hub_h($hubUsername) ?></span>
|
||||
</span>
|
||||
<a href="<?= hub_h($hubCrashesBase) ?>/account-security"
|
||||
class="nav-link"
|
||||
data-i18n-aria="nav.security" data-i18n-title="nav.security"
|
||||
aria-label="Security" title="Security">
|
||||
<span class="nav-icon nav-icon--user" aria-hidden="true"></span>
|
||||
<span class="nav-label" data-i18n="nav.security">Security</span>
|
||||
</a>
|
||||
<a href="<?= hub_h($hubCrashesBase) ?>/logout"
|
||||
class="nav-link nav-link--logout"
|
||||
data-i18n-aria="nav.logout" data-i18n-title="nav.logout"
|
||||
aria-label="Logout" title="Logout">
|
||||
<span class="nav-icon nav-icon--logout" aria-hidden="true"></span>
|
||||
<span class="nav-label" data-i18n="nav.logout">Logout</span>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a href="<?= hub_h($hubCrashesBase) ?>/login"
|
||||
class="nav-link"
|
||||
data-i18n-aria="nav.sign_in" data-i18n-title="nav.sign_in"
|
||||
aria-label="Sign in" title="Sign in">
|
||||
<span class="nav-icon nav-icon--user" aria-hidden="true"></span>
|
||||
<span class="nav-label" data-i18n="nav.sign_in">Sign in</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="landing-scroll" id="landing-scroll" tabindex="-1">
|
||||
|
||||
@@ -1576,6 +1576,51 @@ button.report-tag--filter:hover {
|
||||
.tag-modal-close:hover { color: var(--text); background: var(--row-hover); }
|
||||
.tag-editor--modal { margin: 0; border: none; border-radius: 0; }
|
||||
|
||||
.console-home-intro {
|
||||
margin: 0 0 20px;
|
||||
color: var(--muted);
|
||||
max-width: 42rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.console-home-cards {
|
||||
margin: 0 0 28px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
.console-home-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
.console-home-card--btn {
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
.console-home-card h2 {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.console-home-card p {
|
||||
margin: 0;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.console-home-card .nav-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.console-home-more {
|
||||
margin: 8px 0 12px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
.graphs-app .graphs-quick-links,
|
||||
.console-quick-links {
|
||||
display: flex;
|
||||
|
||||
@@ -11,8 +11,15 @@
|
||||
"lang.label": "Language",
|
||||
"lang.en": "English",
|
||||
"lang.ru": "Russian",
|
||||
"home.title": "Issues console",
|
||||
"home.intro": "Operator console for crash triage and tickets — not the public upload API. Open Issues to browse and group fingerprints, or use the shortcuts below.",
|
||||
"home.title": "Console",
|
||||
"home.intro": "Crash triage, tickets, and session metrics — pick a workspace below.",
|
||||
"home.card_issues": "Issues",
|
||||
"home.card_issues_desc": "Browse and triage crash reports, tags, and fingerprints.",
|
||||
"home.card_new_issue_desc": "Log a new issue or task in one step.",
|
||||
"home.card_tickets_desc": "Roadmap tasks, QA items, and workflow tags.",
|
||||
"home.card_graphs": "Graphs",
|
||||
"home.card_graphs_desc": "Sessions, crashes, and device activity over time.",
|
||||
"home.more": "All services",
|
||||
"reports.title": "Issues",
|
||||
"reports.by_time": "By time",
|
||||
"reports.grouped": "Grouped",
|
||||
@@ -146,6 +153,7 @@
|
||||
"nav.security": "Security",
|
||||
"footer.copyright": "© Anton Afanaasyeu, {year}",
|
||||
"tickets.title": "Tickets",
|
||||
"issues.new": "New issue",
|
||||
"tickets.new": "New ticket",
|
||||
"ticket.create": "Create",
|
||||
"ticket.cancel": "Cancel",
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"nav.reports": "Задачи (Issues)",
|
||||
"nav.tickets": "Задачи",
|
||||
"tickets.title": "Задачи",
|
||||
"tickets.new": "Новая задача",
|
||||
"issues.new": "Новая проблема",
|
||||
"tickets.new": "Новый тикет",
|
||||
"ticket.create": "Создать",
|
||||
"ticket.cancel": "Отмена",
|
||||
"ticket.title": "Заголовок",
|
||||
@@ -20,8 +21,15 @@
|
||||
"lang.label": "Язык",
|
||||
"lang.en": "English",
|
||||
"lang.ru": "Русский",
|
||||
"home.title": "Консоль сбоев",
|
||||
"home.intro": "Операторская консоль для триажа сбоев и тикетов — не публичный API загрузки. Откройте Issues для просмотра и группировки отпечатков или ссылки ниже.",
|
||||
"home.title": "Консоль",
|
||||
"home.intro": "Сбои, задачи и метрики сессий — выберите раздел ниже.",
|
||||
"home.card_issues": "Проблемы",
|
||||
"home.card_issues_desc": "Просмотр и разбор отчётов о сбоях, тегов и отпечатков.",
|
||||
"home.card_new_issue_desc": "Создать новую проблему или задачу.",
|
||||
"home.card_tickets_desc": "Задачи, QA и теги workflow.",
|
||||
"home.card_graphs": "Графики",
|
||||
"home.card_graphs_desc": "Сессии, сбои и активность устройств.",
|
||||
"home.more": "Все сервисы",
|
||||
"reports.title": "Issues",
|
||||
"reports.by_time": "По времени",
|
||||
"reports.grouped": "Группы",
|
||||
|
||||
@@ -1284,6 +1284,20 @@
|
||||
return basePath() + (entityType === 'ticket' ? '/api/ticket_tags.php' : '/api/report_tags.php');
|
||||
}
|
||||
|
||||
const TICKET_META_IDS = ['ticket', 'issue'];
|
||||
|
||||
function isTicketMetaId(id) {
|
||||
return TICKET_META_IDS.indexOf(id) >= 0;
|
||||
}
|
||||
|
||||
function stripTicketMetaTags(tags) {
|
||||
return (tags || []).filter((t) => !isTicketMetaId(t.id));
|
||||
}
|
||||
|
||||
function tagsJson(tags) {
|
||||
return JSON.stringify(tags || []);
|
||||
}
|
||||
|
||||
function createTagEditor(cfg) {
|
||||
const entityType = cfg.entityType === 'ticket' ? 'ticket' : 'report';
|
||||
const state = {
|
||||
@@ -1357,6 +1371,9 @@
|
||||
state.workflow.find((x) => x.id === id) ||
|
||||
state.presets.find((x) => x.id === id);
|
||||
if (!p || state.tags.some((t) => t.id === p.id)) return;
|
||||
if (entityType === 'ticket' && isTicketMetaId(p.id)) {
|
||||
state.tags = stripTicketMetaTags(state.tags);
|
||||
}
|
||||
state.tags.push({ ...p });
|
||||
renderAll();
|
||||
});
|
||||
@@ -1436,6 +1453,7 @@
|
||||
if (els.saveBtn) {
|
||||
els.saveBtn.addEventListener('click', () => {
|
||||
setStatus(t('tag.saving'), true);
|
||||
const sentTags = state.tags.map((t) => ({ ...t }));
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', tagsApiUrl(state.entityType), true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
@@ -1453,7 +1471,8 @@
|
||||
}
|
||||
state.tags = data.tags || [];
|
||||
renderAll();
|
||||
setStatus('Saved', true);
|
||||
const adjusted = tagsJson(sentTags) !== tagsJson(state.tags);
|
||||
setStatus(adjusted ? 'Saved (type/status tags normalized)' : 'Saved', true);
|
||||
if (cfg.onSaved) cfg.onSaved(state.tags, state.entityId);
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
/**
|
||||
* Global "New issue" dialog — one click from nav or any toolbar.
|
||||
* Create ticket/issue dialog — Issues toolbar uses "issue" type tag; Tickets uses "ticket".
|
||||
*/
|
||||
(function () {
|
||||
const META = {
|
||||
ticket: { id: 'ticket', label: 'ticket', bg: '#6366f1' },
|
||||
issue: { id: 'issue', label: 'issue', bg: '#0d9488' },
|
||||
};
|
||||
|
||||
function basePath() {
|
||||
return document.body.getAttribute('data-base-path') || '';
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
function t(key) {
|
||||
if (window.CrashI18n && window.CrashI18n.t) {
|
||||
return window.CrashI18n.t(key);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
function openDialog(kind) {
|
||||
const dialog = document.getElementById('ticket-create-dialog');
|
||||
const form = document.getElementById('ticket-create-form');
|
||||
const status = document.getElementById('ticket-create-status');
|
||||
const heading = document.getElementById('ticket-create-heading');
|
||||
if (!dialog || !form) return;
|
||||
const createKind = kind === 'issue' ? 'issue' : 'ticket';
|
||||
form.dataset.createKind = createKind;
|
||||
if (heading) {
|
||||
heading.textContent = t(createKind === 'issue' ? 'issues.new' : 'tickets.new');
|
||||
}
|
||||
if (status) status.textContent = '';
|
||||
form.reset();
|
||||
if (typeof dialog.showModal === 'function') {
|
||||
@@ -26,10 +44,16 @@
|
||||
const status = document.getElementById('ticket-create-status');
|
||||
if (!dialog || !form) return;
|
||||
|
||||
document.querySelectorAll('.js-new-issue-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
openDialog('issue');
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.js-new-ticket-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
openDialog();
|
||||
openDialog('ticket');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,10 +71,16 @@
|
||||
const titleEl = document.getElementById('ticket-create-title');
|
||||
const briefEl = document.getElementById('ticket-create-brief');
|
||||
const bodyEl = document.getElementById('ticket-create-body');
|
||||
const kind = form.dataset.createKind === 'issue' ? 'issue' : 'ticket';
|
||||
const meta = META[kind] || META.ticket;
|
||||
const payload = {
|
||||
title: titleEl && titleEl.value ? titleEl.value.trim() : '',
|
||||
brief: briefEl && briefEl.value ? briefEl.value.trim() : '',
|
||||
body: bodyEl && bodyEl.value ? bodyEl.value.trim() : '',
|
||||
tags: [
|
||||
meta,
|
||||
{ id: 'open', label: 'open', bg: '#22c55e' },
|
||||
],
|
||||
};
|
||||
if (!payload.title) return;
|
||||
if (status) status.textContent = 'Creating…';
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/** Workflow tags for tickets (must include meta tag {@see META_ID}). */
|
||||
/** Workflow tags for tickets (must include exactly one meta tag: ticket or issue). */
|
||||
final class TicketTagCatalog {
|
||||
public const META_ID = 'ticket';
|
||||
|
||||
/** @var list<string> */
|
||||
public const META_IDS = ['ticket', 'issue'];
|
||||
|
||||
/** @var array<string, array{label:string,bg:string}> */
|
||||
private const META = [
|
||||
'ticket' => ['label' => 'ticket', 'bg' => '#6366f1'],
|
||||
'issue' => ['label' => 'issue', 'bg' => '#0d9488'],
|
||||
];
|
||||
|
||||
/** @var array<string, array{label:string,bg:string}> */
|
||||
private const STATUS = [
|
||||
'open' => ['label' => 'open', 'bg' => '#22c55e'],
|
||||
@@ -25,54 +34,72 @@ final class TicketTagCatalog {
|
||||
|
||||
/** @return list<array{id:string,label:string,bg:string}> */
|
||||
public static function presets(): array {
|
||||
$out = [
|
||||
['id' => self::META_ID, 'label' => 'ticket', 'bg' => '#6366f1'],
|
||||
];
|
||||
$out = [];
|
||||
foreach (self::META as $id => $def) {
|
||||
$out[] = ['id' => $id, 'label' => $def['label'], 'bg' => $def['bg']];
|
||||
}
|
||||
foreach (self::STATUS as $id => $def) {
|
||||
$out[] = ['id' => $id, 'label' => $def['label'], 'bg' => $def['bg']];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function isMetaId(string $id): bool {
|
||||
return isset(self::META[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<mixed> $tags
|
||||
* @return list<array{id:string,label:string,bg:string}>
|
||||
*/
|
||||
public static function normalize(array $tags): array {
|
||||
$normalized = normalize_report_tags($tags);
|
||||
$hasMeta = false;
|
||||
$metaId = null;
|
||||
$hasStatus = false;
|
||||
$rest = [];
|
||||
foreach ($normalized as $t) {
|
||||
if ($t['id'] === self::META_ID) {
|
||||
$hasMeta = true;
|
||||
if (self::isMetaId($t['id'])) {
|
||||
$metaId = $t['id'];
|
||||
continue;
|
||||
}
|
||||
if (isset(self::STATUS[$t['id']])) {
|
||||
$hasStatus = true;
|
||||
}
|
||||
$rest[] = $t;
|
||||
}
|
||||
if (!$hasMeta) {
|
||||
$normalized[] = ['id' => self::META_ID, 'label' => 'ticket', 'bg' => '#6366f1'];
|
||||
if ($metaId === null) {
|
||||
$metaId = self::META_ID;
|
||||
}
|
||||
$meta = self::META[$metaId];
|
||||
$out = [
|
||||
['id' => $metaId, 'label' => $meta['label'], 'bg' => $meta['bg']],
|
||||
];
|
||||
foreach ($rest as $t) {
|
||||
$out[] = $t;
|
||||
}
|
||||
if (!$hasStatus) {
|
||||
$normalized[] = ['id' => 'open', 'label' => 'open', 'bg' => '#22c55e'];
|
||||
$out[] = ['id' => 'open', 'label' => 'open', 'bg' => '#22c55e'];
|
||||
}
|
||||
return $normalized;
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function validate(array $tags): ?string {
|
||||
$normalized = self::normalize($tags);
|
||||
$hasMeta = false;
|
||||
$normalized = normalize_report_tags($tags);
|
||||
$metaCount = 0;
|
||||
$hasStatus = false;
|
||||
foreach ($normalized as $t) {
|
||||
if ($t['id'] === self::META_ID) {
|
||||
$hasMeta = true;
|
||||
if (self::isMetaId($t['id'])) {
|
||||
$metaCount++;
|
||||
}
|
||||
if (isset(self::STATUS[$t['id']])) {
|
||||
$hasStatus = true;
|
||||
}
|
||||
}
|
||||
if (!$hasMeta) {
|
||||
return 'tags must include ticket meta tag';
|
||||
if ($metaCount === 0) {
|
||||
return 'tags must include a type tag (ticket or issue)';
|
||||
}
|
||||
if ($metaCount > 1) {
|
||||
return 'only one type tag allowed: ticket or issue';
|
||||
}
|
||||
if (!$hasStatus) {
|
||||
return 'tags must include a status tag (open, in-progress, closed, …)';
|
||||
|
||||
@@ -62,7 +62,7 @@ final class TicketWorkflow {
|
||||
$keep = [];
|
||||
$statusIds = array_merge(TicketTagCatalog::statusIds(), TicketWorkflow::all());
|
||||
foreach (normalize_report_tags($tags) as $t) {
|
||||
if ($t['id'] === TicketTagCatalog::META_ID) {
|
||||
if (TicketTagCatalog::isMetaId($t['id'])) {
|
||||
$keep[] = $t;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -163,9 +163,7 @@
|
||||
</nav>
|
||||
<main class="main-pane">
|
||||
<?php if (($view ?? 'home') === 'home'): ?>
|
||||
<h1 data-i18n="home.title">Issues console</h1>
|
||||
<p data-i18n="home.intro">Operator console for crash triage and tickets — not the public upload API. Open <strong>Issues</strong> to browse and group fingerprints, or use the shortcuts below.</p>
|
||||
<?php $skip_home_link = true; require __DIR__ . '/partials/console_quick_links.php'; ?>
|
||||
<?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)): ?>
|
||||
@@ -229,7 +227,7 @@
|
||||
<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-ticket-btn" data-i18n="tickets.new">New ticket</button>
|
||||
<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">
|
||||
@@ -537,7 +535,7 @@
|
||||
<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 data-i18n="tickets.new">New ticket</h2>
|
||||
<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">
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
$bp = Auth::basePath();
|
||||
$proj = '/app/androidcast_project';
|
||||
?>
|
||||
<div class="console-home">
|
||||
<div class="toolbar reports-toolbar console-home-toolbar">
|
||||
<h1 data-i18n="home.title">Console</h1>
|
||||
<div class="toolbar-actions">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<p class="console-home-intro" data-i18n="home.intro">Crash triage, tickets, and session metrics — pick a workspace below.</p>
|
||||
|
||||
<div class="cards cards--lift console-home-cards">
|
||||
<a href="<?= h($bp) ?>/?view=reports" class="card card--lift console-home-card">
|
||||
<span class="nav-icon nav-icon--reports" aria-hidden="true"></span>
|
||||
<h2 data-i18n="home.card_issues">Issues</h2>
|
||||
<p class="muted" data-i18n="home.card_issues_desc">Browse and triage crash reports, tags, and fingerprints.</p>
|
||||
</a>
|
||||
<button type="button" class="card card--lift console-home-card console-home-card--btn js-new-issue-btn">
|
||||
<span class="nav-icon nav-icon--reports" aria-hidden="true"></span>
|
||||
<h2 data-i18n="issues.new">New issue</h2>
|
||||
<p class="muted" data-i18n="home.card_new_issue_desc">Log a new issue or task in one step.</p>
|
||||
</button>
|
||||
<a href="<?= h($bp) ?>/?view=tickets" class="card card--lift console-home-card">
|
||||
<span class="nav-icon nav-icon--tickets" aria-hidden="true"></span>
|
||||
<h2 data-i18n="nav.tickets">Tickets</h2>
|
||||
<p class="muted" data-i18n="home.card_tickets_desc">Roadmap tasks, QA items, and workflow tags.</p>
|
||||
</a>
|
||||
<a href="<?= h($proj) ?>/graphs/" class="card card--lift console-home-card">
|
||||
<span class="nav-icon nav-icon--graphs" aria-hidden="true"></span>
|
||||
<h2 data-i18n="home.card_graphs">Graphs</h2>
|
||||
<p class="muted" data-i18n="home.card_graphs_desc">Sessions, crashes, and device activity over time.</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2 class="console-home-more" data-i18n="home.more">All services</h2>
|
||||
<?php $skip_home_link = true; require __DIR__ . '/console_quick_links.php'; ?>
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@ for ($i = 1; $i <= 5; $i++) {
|
||||
<section class="tag-editor" id="tag-editor" data-entity-type="ticket" data-ticket-id="<?= $ticketDbId ?>">
|
||||
<div class="tag-editor-head">
|
||||
<h2 data-i18n="detail.tags">Tags</h2>
|
||||
<p class="muted tag-editor-hint" data-i18n="ticket.tag_hint">Must include <strong>ticket</strong> and a status tag (open, in progress, closed, …).</p>
|
||||
<p class="muted tag-editor-hint" data-i18n="ticket.tag_hint">Include one type tag (<strong>ticket</strong> or <strong>issue</strong>) and a status tag (open, in progress, closed, …). Swap type via suggestions — remove the current type tag, then add the other.</p>
|
||||
</div>
|
||||
<div class="tag-editor-preview" id="tag-editor-preview" aria-live="polite"></div>
|
||||
<?php if ($canEdit): ?>
|
||||
|
||||
Reference in New Issue
Block a user