1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 05:17:39 +03:00

next force snapshot

This commit is contained in:
Anton Afanasyeu
2026-05-30 16:23:01 +02:00
parent eebee1864d
commit 3b0a694aa6
34 changed files with 1759 additions and 130 deletions

View File

@@ -41,6 +41,23 @@ php -S 127.0.0.1:8080 -t public
Open http://127.0.0.1:8080/ — login **admin** / **admin**.
## Google Analytics 4 (optional)
In `config/config.php`:
```php
'analytics' => [
'enabled' => true,
'measurement_id' => 'G-XXXXXXXXXX',
'debug' => false,
],
```
Loads `public/assets/js/analytics.js` from `layout.php` and `login.php`. The project hub at
`/app/androidcast_project/` uses the same script; configure `examples/app_hub/assets/analytics.config.js`.
Events include `page_view`, `console_view` (home, tickets, reports, …), and hub `hub_nav_click` / `hub_open_docs`.
## RBAC (phase 1)
Multi-tenant foundation (no admin UI yet):

View File

@@ -37,4 +37,10 @@ return [
'default_company_slug' => 'default',
'default_company_id' => 1,
],
// Google Analytics 4 (hub + crash console share public/assets/js/analytics.js)
'analytics' => [
'enabled' => false,
'measurement_id' => '', // e.g. G-XXXXXXXXXX
'debug' => false,
],
];

View File

@@ -0,0 +1,145 @@
/**
* Google Analytics 4 for Android Cast web platform (hub + crash console).
* Set window.ANDROIDCAST_ANALYTICS before this script loads:
* { measurementId: 'G-XXXXXXXX', platform: 'hub'|'crashes', debug: false }
*/
(function (global) {
'use strict';
var cfg = global.ANDROIDCAST_ANALYTICS || {};
var measurementId = (cfg.measurementId || '').trim();
var platform = cfg.platform || 'web';
var debug = !!cfg.debug;
function log() {
if (debug && global.console) {
global.console.log.apply(global.console, ['[analytics]'].concat([].slice.call(arguments)));
}
}
function enabled() {
return measurementId.length > 0 && /^G-[A-Z0-9]+$/i.test(measurementId);
}
function ensureGtag(cb) {
if (!enabled()) {
return;
}
global.dataLayer = global.dataLayer || [];
if (!global.gtag) {
global.gtag = function () {
global.dataLayer.push(arguments);
};
}
if (global.__acAnalyticsLoaded) {
if (cb) cb();
return;
}
var s = document.createElement('script');
s.async = true;
s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(measurementId);
s.onload = function () {
global.__acAnalyticsLoaded = true;
global.gtag('js', new Date());
global.gtag('config', measurementId, {
send_page_view: false,
anonymize_ip: true
});
log('loaded', measurementId);
if (cb) cb();
};
document.head.appendChild(s);
}
function pagePath() {
var base = (cfg.basePath || '').replace(/\/$/, '');
var path = global.location.pathname || '/';
if (base && path.indexOf(base) === 0) {
path = path.slice(base.length) || '/';
}
return path;
}
function pageView(extra) {
if (!enabled()) return;
ensureGtag(function () {
var params = {
page_title: document.title || '',
page_location: global.location.href,
page_path: pagePath(),
platform: platform
};
if (extra) {
for (var k in extra) {
if (Object.prototype.hasOwnProperty.call(extra, k)) params[k] = extra[k];
}
}
global.gtag('event', 'page_view', params);
log('page_view', params);
});
}
function trackEvent(name, params) {
if (!enabled() || !name) return;
ensureGtag(function () {
var payload = params || {};
payload.platform = platform;
global.gtag('event', name, payload);
log('event', name, payload);
});
}
function bindConsoleView() {
var body = document.body;
if (!body) return;
var view = body.getAttribute('data-view');
if (view) {
trackEvent('console_view', { view_name: view });
}
}
function bindHubInteractions() {
document.addEventListener('click', function (ev) {
var t = ev.target;
if (!(t instanceof Element)) return;
var card = t.closest('.hub-card, .hub-card-link');
if (card) {
var label = card.getAttribute('data-analytics-label')
|| (card.textContent || '').trim().slice(0, 64);
trackEvent('hub_nav_click', { link_text: label });
return;
}
if (t.closest('[data-open-docs], #hub-compass-layer, .hub-logo-layer--compass')) {
trackEvent('hub_open_docs', { source: 'compass' });
}
if (t.closest('[data-close-docs]')) {
trackEvent('hub_close_docs', {});
}
});
}
function init() {
if (!enabled()) {
log('disabled (no measurement id)');
return;
}
pageView();
if (platform === 'crashes') {
bindConsoleView();
} else if (platform === 'hub') {
bindHubInteractions();
}
}
global.AndroidCastAnalytics = {
enabled: enabled,
pageView: pageView,
trackEvent: trackEvent
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})(typeof window !== 'undefined' ? window : this);

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/** Emits GA4 config + shared analytics.js for PHP views. */
final class AnalyticsHead {
public static function render(string $platform = 'crashes'): void {
$enabled = (bool) cfg('analytics.enabled', false);
$measurementId = trim((string) cfg('analytics.measurement_id', ''));
if (!$enabled || $measurementId === '') {
return;
}
$basePath = Auth::basePath();
$config = [
'measurementId' => $measurementId,
'platform' => $platform,
'basePath' => $basePath,
'debug' => (bool) cfg('analytics.debug', false),
];
$json = json_encode($config, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP);
if ($json === false) {
return;
}
echo '<script>window.ANDROIDCAST_ANALYTICS=', $json, ';</script>', "\n";
echo '<script src="', h($basePath), '/assets/js/analytics.js" defer></script>', "\n";
}
}

View File

@@ -44,6 +44,7 @@ require_once __DIR__ . '/TicketAttachmentRepository.php';
require_once __DIR__ . '/TicketCommentRepository.php';
require_once __DIR__ . '/UserRepository.php';
require_once __DIR__ . '/TicketRepository.php';
require_once __DIR__ . '/AnalyticsHead.php';
function cfg(string $key, $default = null) {
global $config;

View File

@@ -31,6 +31,7 @@
<?php if (in_array($view ?? '', ['tickets', 'ticket'], true)): ?>
<script src="<?= h(Auth::basePath()) ?>/assets/js/tickets.js" defer></script>
<?php endif; ?>
<?php AnalyticsHead::render('crashes'); ?>
</head>
<body data-base-path="<?= h(Auth::basePath()) ?>"
data-view="<?= h($view ?? 'home') ?>"

View File

@@ -26,6 +26,7 @@ $bp = Auth::basePath();
</script>
<link rel="stylesheet" href="<?= h($bp) ?>/assets/css/app.css">
<script src="<?= h($bp) ?>/assets/js/i18n.js" defer></script>
<?php AnalyticsHead::render('crashes_login'); ?>
</head>
<body class="login-page" data-base-path="<?= h($bp) ?>">
<div class="login-locale">