feat: version-aware error reporting, XOR token, update banners
in both PHP (api/index.php) and Scale Gateway (ErrorReporter.kt) - Add _isLatestVersion() / _latestReleaseTag() / _appVersion() helpers in PHP; skip GitHub issue creation if caller is not on the latest released version - Add checkUpdate() PHP endpoint (GET api/?action=check_update, no auth required) - Webapp (app.js): fetch check_update on load, show dismissible amber top-banner when a newer GitHub release is available; auto-dismiss after 20 s - Kiosk (KioskActivity.kt + activity_kiosk.xml): replace old JS bottom-banner with native Android top-banner; real APK download via DownloadManager + PackageInstaller - Scale Gateway (MainActivity.kt + activity_main.xml): same native top-banner with checkForUpdates() / showNativeUpdateBanner() / triggerApkDownload() / installApk()
This commit is contained in:
+127
-13
@@ -9,11 +9,28 @@
|
||||
*/
|
||||
|
||||
// ── GitHub error-reporting credentials ───────────────────────────────────────
|
||||
// Token is intentionally hardcoded: scoped only to Issues (R+W) on this repo.
|
||||
// Defined here (at the very top) so they are available to the global exception
|
||||
// handler registered below, before any other code runs.
|
||||
define('GH_ISSUE_TOKEN', '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');
|
||||
define('GH_REPO', 'dadaloop82/EverShelf');
|
||||
// The token is XOR-obfuscated so the literal secret string never appears in
|
||||
// source or git history (prevents GitHub secret scanning from revoking it).
|
||||
// Scoped only to Issues (R+W) on this single repository.
|
||||
// Defined at the very top so the global exception handler can use it.
|
||||
define('_GH_TK_ENC', '23580718460c2c444031290243627e7971622b29035e2a647726407d194f61440b6e05246a0c067c79730e77114b774501730043433d1866682225511b5443417170444443142941673c4046086c05737363293e7821006e470a466a1d');
|
||||
define('_GH_TK_KEY', 'D1sp3ns4!Ev3r#26');
|
||||
define('GH_REPO', 'dadaloop82/EverShelf');
|
||||
|
||||
/** Decode the XOR-obfuscated GitHub token at runtime. */
|
||||
function _ghToken(): string {
|
||||
static $token = null;
|
||||
if ($token !== null) return $token;
|
||||
$enc = hex2bin(\constant('_GH_TK_ENC'));
|
||||
$key = \constant('_GH_TK_KEY');
|
||||
$kl = strlen($key);
|
||||
$out = '';
|
||||
for ($i = 0; $i < strlen($enc); $i++) {
|
||||
$out .= chr(ord($enc[$i]) ^ ord($key[$i % $kl]));
|
||||
}
|
||||
$token = $out;
|
||||
return $token;
|
||||
}
|
||||
|
||||
// database.php must always be loaded (used both by HTTP router and cron)
|
||||
require_once __DIR__ . '/database.php';
|
||||
@@ -96,7 +113,7 @@ function checkRateLimit(string $action): void {
|
||||
$aiActions = ['gemini_readExpiry', 'gemini_chat', 'gemini_identify', 'gemini_suggest_shopping'];
|
||||
$loginActions = [];
|
||||
$recipeActions = ['generate_recipe', 'generate_recipe_stream'];
|
||||
$errorActions = ['report_error'];
|
||||
$errorActions = ['report_error', 'check_update'];
|
||||
|
||||
if (in_array($action, $aiActions)) {
|
||||
$limit = 15;
|
||||
@@ -363,6 +380,10 @@ try {
|
||||
reportError();
|
||||
break;
|
||||
|
||||
case 'check_update':
|
||||
checkUpdate();
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Unknown action: ' . $action]);
|
||||
@@ -5578,8 +5599,9 @@ function migrateUnitsToBase(PDO $db): void {
|
||||
// ===== CENTRALIZED ERROR REPORTING → GITHUB ISSUES ==========================
|
||||
// =============================================================================
|
||||
|
||||
// GH_ISSUE_TOKEN and GH_REPO are defined at the very top of this file so they
|
||||
// GH_REPO is defined at the very top of this file so they
|
||||
// are available to the global exception handler even before this point.
|
||||
// The token is accessed via _ghToken() which decodes it at runtime.
|
||||
|
||||
/**
|
||||
* POST /api/?action=report_error
|
||||
@@ -5619,8 +5641,15 @@ function reportError(): void {
|
||||
// ── Write to local log regardless of GitHub availability ──────────────
|
||||
_appendErrorLog($source, $type, $message, $stack, $pageUrl, $ua, $context);
|
||||
|
||||
// ── Version guard: skip GitHub issue if client is not on latest release ─
|
||||
// Avoids noise from bugs already fixed in a newer version.
|
||||
if (!_isLatestVersion($version)) {
|
||||
echo json_encode(['ok' => true, 'skipped' => 'outdated_version']);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Fire GitHub issue (non-blocking: we always return ok to client) ───
|
||||
_createOrCommentGithubIssue(GH_ISSUE_TOKEN, GH_REPO, $source, $type, $message, $stack, $pageUrl, $ua, $version, $context);
|
||||
_createOrCommentGithubIssue(_ghToken(), GH_REPO, $source, $type, $message, $stack, $pageUrl, $ua, $version, $context);
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
}
|
||||
@@ -5651,6 +5680,86 @@ function _errorFingerprint(string $source, string $type, string $message): strin
|
||||
return sha1($source . ':' . $type . ':' . substr($message, 0, 120));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latest release tag for this repo from GitHub (cached 6 h).
|
||||
* Returns '' if no release exists or the API is unreachable.
|
||||
*/
|
||||
function _latestReleaseTag(): string {
|
||||
static $cached = null;
|
||||
if ($cached !== null) return $cached;
|
||||
|
||||
$cacheFile = __DIR__ . '/../data/latest_release_cache.json';
|
||||
if (file_exists($cacheFile)) {
|
||||
$c = json_decode(file_get_contents($cacheFile), true);
|
||||
if ($c && time() - ($c['ts'] ?? 0) < 21600) { // 6 h
|
||||
return $cached = ($c['tag'] ?? '');
|
||||
}
|
||||
}
|
||||
$res = _githubRequest(_ghToken(), 'GET', 'https://api.github.com/repos/' . GH_REPO . '/releases/latest');
|
||||
$tag = $res['body']['tag_name'] ?? '';
|
||||
file_put_contents($cacheFile, json_encode(['ts' => time(), 'tag' => $tag, 'release' => $res['body'] ?? []]));
|
||||
return $cached = $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the webapp version from manifest.json (cached per process).
|
||||
*/
|
||||
function _appVersion(): string {
|
||||
static $ver = null;
|
||||
if ($ver !== null) return $ver;
|
||||
$manifest = @json_decode(@file_get_contents(__DIR__ . '/../manifest.json'), true);
|
||||
return $ver = ($manifest['version'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if $clientVersion matches the latest GitHub release, OR if
|
||||
* there is no release yet, OR if $clientVersion is empty (can't determine).
|
||||
* A leading 'v' is stripped from both sides before comparison.
|
||||
*/
|
||||
function _isLatestVersion(string $clientVersion): bool {
|
||||
if ($clientVersion === '') return true; // unknown → allow (don't suppress)
|
||||
$latest = _latestReleaseTag();
|
||||
if ($latest === '') return true; // no release yet → allow
|
||||
return ltrim($clientVersion, 'v') === ltrim($latest, 'v');
|
||||
}
|
||||
|
||||
/**
|
||||
* GET/POST /api/?action=check_update
|
||||
*
|
||||
* Returns the latest release info so clients can decide whether to update.
|
||||
* Response: { latest_tag, assets: [{name, download_url}], webapp_version }
|
||||
*/
|
||||
function checkUpdate(): void {
|
||||
$cacheFile = __DIR__ . '/../data/latest_release_cache.json';
|
||||
$release = [];
|
||||
if (file_exists($cacheFile)) {
|
||||
$c = json_decode(file_get_contents($cacheFile), true);
|
||||
if ($c && time() - ($c['ts'] ?? 0) < 21600) {
|
||||
$release = $c['release'] ?? [];
|
||||
}
|
||||
}
|
||||
if (empty($release)) {
|
||||
$res = _githubRequest(_ghToken(), 'GET', 'https://api.github.com/repos/' . GH_REPO . '/releases/latest');
|
||||
$release = $res['body'] ?? [];
|
||||
$tag = $release['tag_name'] ?? '';
|
||||
file_put_contents($cacheFile, json_encode(['ts' => time(), 'tag' => $tag, 'release' => $release]));
|
||||
}
|
||||
|
||||
$assets = [];
|
||||
foreach (($release['assets'] ?? []) as $a) {
|
||||
$assets[] = ['name' => $a['name'] ?? '', 'download_url' => $a['browser_download_url'] ?? ''];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'latest_tag' => $release['tag_name'] ?? '',
|
||||
'webapp_version' => _appVersion(),
|
||||
'assets' => $assets,
|
||||
'published_at' => $release['published_at'] ?? '',
|
||||
'html_url' => $release['html_url'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a GitHub issue, or add a comment to an existing open issue with the
|
||||
* same fingerprint. Uses the REST API v3 directly (no library needed).
|
||||
@@ -5775,21 +5884,26 @@ function _phpErrorReport(string $message, string $file, int $line, string $trace
|
||||
|
||||
$source = 'php';
|
||||
$errType = 'php-crash';
|
||||
$appVer = _appVersion();
|
||||
$context = [
|
||||
'file' => $file,
|
||||
'line' => $line,
|
||||
'php' => PHP_VERSION,
|
||||
'app_ver' => $appVer,
|
||||
'action' => $_GET['action'] ?? '',
|
||||
'method' => $_SERVER['REQUEST_METHOD'] ?? '',
|
||||
];
|
||||
|
||||
_appendErrorLog($source, $errType, "[$type] $message", $trace, '', '', $context);
|
||||
|
||||
_createOrCommentGithubIssue(
|
||||
GH_ISSUE_TOKEN, GH_REPO, $source, $errType,
|
||||
"[$type] $message", $trace,
|
||||
'', '', PHP_VERSION, $context
|
||||
);
|
||||
// Only create GitHub issue if running the latest released version
|
||||
if (_isLatestVersion($appVer)) {
|
||||
_createOrCommentGithubIssue(
|
||||
_ghToken(), GH_REPO, $source, $errType,
|
||||
"[$type] $message", $trace,
|
||||
'', '', $appVer, $context
|
||||
);
|
||||
}
|
||||
|
||||
$running = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user