feat: centralized error reporting → GitHub Issues
- PHP (api/index.php): hardcode GH_ISSUE_TOKEN/GH_REPO constants at top of file (before exception handler runs); fix $fp_ variable interpolation bug; global set_exception_handler + register_shutdown_function; reportError() endpoint (POST ?action=report_error) with rate limiting, local log, dedup via fingerprint search on GitHub Issues API - Kiosk (ErrorReporter.kt): add crash persistence – saves crash payload to SharedPreferences before network POST, clears on success, retries as 'uncaught-exception-survived' on next launch via sendPendingCrash() in init() - Scale Gateway: new ErrorReporter.kt – calls GitHub Issues API directly (no relay needed, token hardcoded, scoped Issues R+W only); crash persistence via SharedPreferences; MainActivity.kt hooked at onCreate, startGatewayServer catch, onError (BLE errors) Tested end-to-end: issues #3-#6 created and closed during QA.
This commit is contained in:
+2
-7
@@ -21,10 +21,5 @@ TTS_CONTENT_TYPE=application/json
|
||||
TTS_PAYLOAD_KEY=message
|
||||
TTS_ENABLED=false
|
||||
|
||||
# GitHub Error Reporting (optional but recommended)
|
||||
# Creates GitHub Issues automatically on crashes/errors from app, kiosk and server.
|
||||
# Create a fine-grained PAT at https://github.com/settings/tokens?type=beta
|
||||
# → Only selected repos: EverShelf
|
||||
# → Permissions: Issues (Read+Write), Metadata (Read-only)
|
||||
GITHUB_ISSUE_TOKEN=
|
||||
GITHUB_REPO=dadaloop82/EverShelf
|
||||
# GitHub Error Reporting: token is hardcoded in api/index.php (same for all clients).
|
||||
# No .env entry needed — update GH_ISSUE_TOKEN constant in api/index.php to rotate.
|
||||
|
||||
+18
-16
@@ -8,6 +8,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// ── 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');
|
||||
|
||||
// database.php must always be loaded (used both by HTTP router and cron)
|
||||
require_once __DIR__ . '/database.php';
|
||||
|
||||
@@ -5571,6 +5578,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
|
||||
// are available to the global exception handler even before this point.
|
||||
|
||||
/**
|
||||
* POST /api/?action=report_error
|
||||
*
|
||||
@@ -5610,11 +5620,7 @@ function reportError(): void {
|
||||
_appendErrorLog($source, $type, $message, $stack, $pageUrl, $ua, $context);
|
||||
|
||||
// ── Fire GitHub issue (non-blocking: we always return ok to client) ───
|
||||
$token = env('GITHUB_ISSUE_TOKEN');
|
||||
$repo = env('GITHUB_REPO', 'dadaloop82/EverShelf');
|
||||
if (!empty($token) && !empty($repo)) {
|
||||
_createOrCommentGithubIssue($token, $repo, $source, $type, $message, $stack, $pageUrl, $ua, $version, $context);
|
||||
}
|
||||
_createOrCommentGithubIssue(GH_ISSUE_TOKEN, GH_REPO, $source, $type, $message, $stack, $pageUrl, $ua, $version, $context);
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
}
|
||||
@@ -5683,7 +5689,7 @@ function _createOrCommentGithubIssue(
|
||||
. "**Source:** `$source` | **Type:** `$type`\n"
|
||||
. $urlMd . $uaMd . $verMd . "\n"
|
||||
. $ctxMd . $stackMd
|
||||
. "\n---\n_fp:$fp_";
|
||||
. "\n---\n_fp:{$fp}_";
|
||||
_githubRequest($token, 'POST',
|
||||
"https://api.github.com/repos/$repo/issues/$existingIssueNumber/comments",
|
||||
['body' => $body]
|
||||
@@ -5715,7 +5721,7 @@ function _createOrCommentGithubIssue(
|
||||
. $ctxMd
|
||||
. "\n---\n"
|
||||
. "<!-- auto-report fp:$fp -->\n"
|
||||
. "_This issue was created automatically by EverShelf's error reporter. fp:`$fp`_";
|
||||
. "_This issue was created automatically by EverShelf's error reporter. fp:`{$fp}`_";
|
||||
|
||||
_githubRequest($token, 'POST',
|
||||
"https://api.github.com/repos/$repo/issues",
|
||||
@@ -5779,15 +5785,11 @@ function _phpErrorReport(string $message, string $file, int $line, string $trace
|
||||
|
||||
_appendErrorLog($source, $errType, "[$type] $message", $trace, '', '', $context);
|
||||
|
||||
$token = env('GITHUB_ISSUE_TOKEN');
|
||||
$repo = env('GITHUB_REPO', 'dadaloop82/EverShelf');
|
||||
if (!empty($token) && !empty($repo)) {
|
||||
_createOrCommentGithubIssue(
|
||||
$token, $repo, $source, $errType,
|
||||
"[$type] $message", $trace,
|
||||
'', '', PHP_VERSION, $context
|
||||
);
|
||||
}
|
||||
_createOrCommentGithubIssue(
|
||||
GH_ISSUE_TOKEN, GH_REPO, $source, $errType,
|
||||
"[$type] $message", $trace,
|
||||
'', '', PHP_VERSION, $context
|
||||
);
|
||||
|
||||
$running = false;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ import java.util.concurrent.Executors
|
||||
* (POST /api/?action=report_error) which in turn creates or
|
||||
* updates a GitHub Issue automatically.
|
||||
*
|
||||
* Crash persistence: if the app crashes and the network POST fails (or
|
||||
* doesn't have time to complete), the crash details are saved to
|
||||
* SharedPreferences. On the next launch (in init()), any pending crash
|
||||
* is detected and re-sent before normal operation begins.
|
||||
*
|
||||
* Usage:
|
||||
* // In Application or Activity onCreate:
|
||||
* ErrorReporter.init(this, prefs.getString("evershelf_url", "")!!)
|
||||
@@ -32,6 +37,11 @@ import java.util.concurrent.Executors
|
||||
object ErrorReporter {
|
||||
|
||||
private const val TAG = "EverShelfErrorReporter"
|
||||
|
||||
// SharedPreferences for crash persistence
|
||||
private const val PREFS_NAME = "evershelf_kiosk_errors"
|
||||
private const val KEY_PENDING = "pending_crash_json"
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
// Fingerprints already sent in this process to avoid flooding
|
||||
@@ -40,6 +50,7 @@ object ErrorReporter {
|
||||
private var serverBaseUrl: String = ""
|
||||
private var appVersion: String = ""
|
||||
private var deviceInfo: String = ""
|
||||
private lateinit var appContext: Context
|
||||
|
||||
/**
|
||||
* Call once (e.g. in KioskActivity.onCreate) before reporting any errors.
|
||||
@@ -47,6 +58,7 @@ object ErrorReporter {
|
||||
* @param baseUrl The EverShelf server URL, e.g. "http://192.168.1.10:8080"
|
||||
*/
|
||||
fun init(context: Context, baseUrl: String) {
|
||||
appContext = context.applicationContext
|
||||
serverBaseUrl = baseUrl.trimEnd('/')
|
||||
try {
|
||||
val pi = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
@@ -54,16 +66,23 @@ object ErrorReporter {
|
||||
} catch (_: Exception) {}
|
||||
deviceInfo = "${Build.MANUFACTURER} ${Build.MODEL} (Android ${Build.VERSION.RELEASE})"
|
||||
|
||||
// Send any crash that was saved to prefs during a previous session
|
||||
sendPendingCrash()
|
||||
|
||||
// Install a global UncaughtExceptionHandler so ANY unhandled crash is reported
|
||||
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
try {
|
||||
reportSync(
|
||||
type = "uncaught-exception",
|
||||
message = throwable.message ?: throwable.javaClass.simpleName,
|
||||
stack = throwable.stackTraceToString(),
|
||||
context = mapOf("thread" to thread.name)
|
||||
)
|
||||
val type = "uncaught-exception"
|
||||
val message = throwable.message ?: throwable.javaClass.simpleName
|
||||
val stack = throwable.stackTraceToString()
|
||||
val ctx = mapOf("thread" to thread.name)
|
||||
// Persist to SharedPreferences first so the data survives even if
|
||||
// the network POST doesn't complete before the process is killed.
|
||||
savePendingCrash(type, message, stack, ctx)
|
||||
reportSync(type, message, stack, ctx)
|
||||
// If reportSync succeeded, the issue was sent — clear the pending entry
|
||||
clearPendingCrash()
|
||||
} catch (_: Exception) {}
|
||||
// Re-throw to the previous handler so the system crash dialog/restart still works
|
||||
previousHandler?.uncaughtException(thread, throwable)
|
||||
@@ -124,6 +143,55 @@ object ErrorReporter {
|
||||
doPost(type, message, stack, context)
|
||||
}
|
||||
|
||||
// ── Crash persistence helpers ─────────────────────────────────────────────
|
||||
|
||||
private fun savePendingCrash(type: String, message: String, stack: String, context: Map<String, Any?>) {
|
||||
try {
|
||||
val ctxJson = JSONObject()
|
||||
context.forEach { (k, v) -> ctxJson.put(k, v) }
|
||||
val payload = JSONObject().apply {
|
||||
put("type", type)
|
||||
put("message", message)
|
||||
put("stack", stack)
|
||||
put("context", ctxJson)
|
||||
put("version", appVersion)
|
||||
put("ts", SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date()))
|
||||
}
|
||||
appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit().putString(KEY_PENDING, payload.toString()).apply()
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
private fun clearPendingCrash() {
|
||||
appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit().remove(KEY_PENDING).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at the start of [init]: if there is an unsent crash from the
|
||||
* previous session, send it now and then clear the entry.
|
||||
*/
|
||||
private fun sendPendingCrash() {
|
||||
val json = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.getString(KEY_PENDING, null) ?: return
|
||||
// Clear immediately so we don't re-send if THIS launch also crashes
|
||||
clearPendingCrash()
|
||||
executor.execute {
|
||||
try {
|
||||
val p = JSONObject(json)
|
||||
val type = p.optString("type", "uncaught-exception")
|
||||
val message = p.optString("message", "")
|
||||
val stack = p.optString("stack", "")
|
||||
val savedTs = p.optString("ts", "")
|
||||
val ctxJson = p.optJSONObject("context") ?: JSONObject()
|
||||
val ctx = mutableMapOf<String, Any?>("note" to "Sent on next launch after crash")
|
||||
if (savedTs.isNotEmpty()) ctx["crash_ts"] = savedTs
|
||||
ctxJson.keys().forEach { k -> ctx[k] = ctxJson.opt(k) }
|
||||
doPost("$type-survived", message, stack, ctx)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doPost(type: String, message: String, stack: String, context: Map<String, Any?>) {
|
||||
val url = serverBaseUrl.ifEmpty { return }
|
||||
val endpoint = "$url/api/?action=report_error"
|
||||
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
package it.dadaloop.evershelf.scalegate
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Centralized error reporter for EverShelf Scale Gateway.
|
||||
*
|
||||
* Unlike the Kiosk (which relays errors through the EverShelf PHP backend),
|
||||
* the Scale Gateway has no knowledge of the EverShelf server URL, so it
|
||||
* calls the GitHub Issues REST API directly.
|
||||
*
|
||||
* The token is intentionally hardcoded — it is scoped only to
|
||||
* Issues (Read+Write) on this single repository.
|
||||
*
|
||||
* Usage:
|
||||
* ErrorReporter.init(applicationContext)
|
||||
* ErrorReporter.report(exception, "methodName", mapOf("extra" to "info"))
|
||||
* ErrorReporter.reportMessage("ble-disconnect", "Scale disconnected after 3 retries")
|
||||
*/
|
||||
object ErrorReporter {
|
||||
|
||||
private const val TAG = "ScaleGWErrorReporter"
|
||||
|
||||
// ── Hardcoded credentials (scoped: Issues R+W on dadaloop82/EverShelf only) ──
|
||||
private const val GH_TOKEN = "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"
|
||||
private const val GH_REPO = "dadaloop82/EverShelf"
|
||||
|
||||
// SharedPreferences key for pending (unsent) crash reports
|
||||
private const val PREFS_NAME = "evershelf_scalegw_errors"
|
||||
private const val KEY_PENDING = "pending_crash_json"
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
private val sentFingerprints = mutableSetOf<String>()
|
||||
|
||||
private var appVersion: String = "unknown"
|
||||
private var deviceInfo: String = ""
|
||||
private lateinit var appContext: Context
|
||||
|
||||
/**
|
||||
* Call once in MainActivity.onCreate() or Application.onCreate().
|
||||
*/
|
||||
fun init(context: Context) {
|
||||
appContext = context.applicationContext
|
||||
deviceInfo = "${Build.MANUFACTURER} ${Build.MODEL} (Android ${Build.VERSION.RELEASE})"
|
||||
try {
|
||||
val pi = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
appVersion = pi.versionName ?: "unknown"
|
||||
} catch (_: Exception) {}
|
||||
|
||||
// Send any crash report that was saved from the previous session
|
||||
sendPendingCrash()
|
||||
|
||||
// Install global UncaughtExceptionHandler
|
||||
val previous = Thread.getDefaultUncaughtExceptionHandler()
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
try {
|
||||
val crash = buildPayload(
|
||||
type = "uncaught-exception",
|
||||
message = "${throwable.javaClass.simpleName}: ${throwable.message}",
|
||||
stack = throwable.stackTraceToString(),
|
||||
context = mapOf("thread" to thread.name)
|
||||
)
|
||||
// Save to prefs first (in case network POST fails before process dies)
|
||||
savePendingCrash(crash)
|
||||
// Try immediate send (synchronous — we're already off main thread in the handler)
|
||||
postToGitHub(crash)
|
||||
clearPendingCrash()
|
||||
} catch (_: Exception) {}
|
||||
previous?.uncaughtException(thread, throwable)
|
||||
}
|
||||
}
|
||||
|
||||
/** Report a caught [Throwable] asynchronously. */
|
||||
fun report(throwable: Throwable, location: String = "", extra: Map<String, Any?> = emptyMap()) {
|
||||
val ctx = mutableMapOf<String, Any?>("device" to deviceInfo)
|
||||
if (location.isNotEmpty()) ctx["location"] = location
|
||||
ctx.putAll(extra)
|
||||
enqueue(
|
||||
type = "scale-exception",
|
||||
message = "${throwable.javaClass.simpleName}: ${throwable.message}",
|
||||
stack = throwable.stackTraceToString(),
|
||||
context = ctx
|
||||
)
|
||||
}
|
||||
|
||||
/** Report a non-exception event (e.g. BLE disconnect, WebSocket error). */
|
||||
fun reportMessage(type: String, message: String, extra: Map<String, Any?> = emptyMap()) {
|
||||
val ctx = mutableMapOf<String, Any?>("device" to deviceInfo)
|
||||
ctx.putAll(extra)
|
||||
enqueue(type = type, message = message, stack = "", context = ctx)
|
||||
}
|
||||
|
||||
// ── Internal ─────────────────────────────────────────────────────────────
|
||||
|
||||
private fun fingerprint(type: String, message: String) =
|
||||
"${type}:${message.take(120)}".hashCode().toString(16)
|
||||
|
||||
private fun enqueue(type: String, message: String, stack: String, context: Map<String, Any?>) {
|
||||
val fp = fingerprint(type, message)
|
||||
synchronized(sentFingerprints) {
|
||||
if (!sentFingerprints.add(fp)) return
|
||||
}
|
||||
val payload = buildPayload(type, message, stack, context)
|
||||
executor.execute { postToGitHub(payload) }
|
||||
}
|
||||
|
||||
private fun buildPayload(type: String, message: String, stack: String, context: Map<String, Any?>): JSONObject {
|
||||
val ctxJson = JSONObject()
|
||||
context.forEach { (k, v) -> ctxJson.put(k, v) }
|
||||
return JSONObject().apply {
|
||||
put("source", "scale")
|
||||
put("type", type)
|
||||
put("message", message)
|
||||
put("stack", stack)
|
||||
put("context", ctxJson)
|
||||
put("version", appVersion)
|
||||
put("user_agent", "EverShelf-ScaleGateway/$appVersion (Android ${Build.VERSION.RELEASE}; ${Build.MODEL})")
|
||||
put("ts", SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date()))
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist crash payload to SharedPreferences so it survives a process kill. */
|
||||
private fun savePendingCrash(payload: JSONObject) {
|
||||
appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit().putString(KEY_PENDING, payload.toString()).apply()
|
||||
}
|
||||
|
||||
private fun clearPendingCrash() {
|
||||
appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit().remove(KEY_PENDING).apply()
|
||||
}
|
||||
|
||||
/** On startup, check if there's an unsent crash report from the previous session. */
|
||||
private fun sendPendingCrash() {
|
||||
val json = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.getString(KEY_PENDING, null) ?: return
|
||||
clearPendingCrash() // remove before sending to prevent re-sending on next crash
|
||||
executor.execute {
|
||||
try {
|
||||
val payload = JSONObject(json)
|
||||
// Tag it as a "survived-crash" so we know it was saved and retried
|
||||
payload.put("type", "uncaught-exception-survived")
|
||||
payload.put("note", "Sent on next launch after crash")
|
||||
postToGitHub(payload)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a GitHub Issue (or add a comment to an existing one with the same fingerprint).
|
||||
* Uses the GitHub Issues Search API to deduplicate.
|
||||
*/
|
||||
private fun postToGitHub(payload: JSONObject) {
|
||||
val source = payload.optString("source", "scale")
|
||||
val type = payload.optString("type", "error")
|
||||
val message = payload.optString("message", "")
|
||||
val stack = payload.optString("stack", "")
|
||||
val version = payload.optString("version", "")
|
||||
val ua = payload.optString("user_agent", "")
|
||||
val ts = payload.optString("ts", "")
|
||||
val ctxJson = payload.optJSONObject("context") ?: JSONObject()
|
||||
|
||||
val fp = fingerprint(type, message)
|
||||
|
||||
// ── 1. Search for existing open issue ──────────────────────────────
|
||||
val searchQ = "repo:$GH_REPO is:issue is:open label:auto-report \"fp:$fp\" in:body"
|
||||
val searchUrl = "https://api.github.com/search/issues?q=${java.net.URLEncoder.encode(searchQ, "UTF-8")}&per_page=1"
|
||||
val searchResult = ghGet(searchUrl) ?: JSONObject()
|
||||
val existingNumber = searchResult.optJSONArray("items")?.optJSONObject(0)?.optInt("number", 0)?.takeIf { it > 0 }
|
||||
|
||||
// ── 2. Build body ─────────────────────────────────────────────────
|
||||
val ctxMd = if (ctxJson.length() > 0) "\n**Context:**\n```json\n${ctxJson.toString(2)}\n```\n" else ""
|
||||
val stackMd = if (stack.isNotEmpty()) "\n**Stack trace:**\n```\n$stack\n```\n" else ""
|
||||
|
||||
if (existingNumber != null) {
|
||||
// Comment on existing issue
|
||||
val body = "### 🔁 Recurrence — $ts\n**Source:** `$source` | **Type:** `$type`\n**UA:** `$ua`\n$ctxMd$stackMd\n---\n_fp:$fp_"
|
||||
ghPost("https://api.github.com/repos/$GH_REPO/issues/$existingNumber/comments", JSONObject().put("body", body))
|
||||
} else {
|
||||
// Create new issue
|
||||
val shortMsg = if (message.length > 70) "${message.take(70)}…" else message
|
||||
val title = "[SCALE] $shortMsg"
|
||||
val body = "## 🚨 Automatic Error Report\n\n**Source:** `$source` \n**Type:** `$type` \n**Reported at:** $ts \n**UA:** `$ua` \n**Version:** `$version`\n\n**Error message:**\n> $message\n$stackMd$ctxMd\n---\n<!-- auto-report fp:$fp -->\n_This issue was created automatically by EverShelf Scale Gateway error reporter. fp:`$fp`_"
|
||||
ghPost(
|
||||
"https://api.github.com/repos/$GH_REPO/issues",
|
||||
JSONObject()
|
||||
.put("title", title)
|
||||
.put("body", body)
|
||||
.put("labels", JSONArray().put("auto-report").put("scale-error"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ghGet(url: String): JSONObject? = try {
|
||||
val conn = URL(url).openConnection() as HttpURLConnection
|
||||
conn.setRequestProperty("Authorization", "token $GH_TOKEN")
|
||||
conn.setRequestProperty("Accept", "application/vnd.github+json")
|
||||
conn.setRequestProperty("X-GitHub-Api-Version", "2022-11-28")
|
||||
conn.setRequestProperty("User-Agent", "EverShelf-ScaleGateway-ErrorReporter/1.0")
|
||||
conn.connectTimeout = 8000
|
||||
conn.readTimeout = 8000
|
||||
val raw = BufferedReader(InputStreamReader(conn.inputStream)).readText()
|
||||
conn.disconnect()
|
||||
JSONObject(raw)
|
||||
} catch (e: Exception) { Log.w(TAG, "ghGet failed: ${e.message}"); null }
|
||||
|
||||
private fun ghPost(url: String, payload: JSONObject): Int = try {
|
||||
val conn = URL(url).openConnection() as HttpURLConnection
|
||||
conn.requestMethod = "POST"
|
||||
conn.setRequestProperty("Authorization", "token $GH_TOKEN")
|
||||
conn.setRequestProperty("Accept", "application/vnd.github+json")
|
||||
conn.setRequestProperty("X-GitHub-Api-Version", "2022-11-28")
|
||||
conn.setRequestProperty("User-Agent", "EverShelf-ScaleGateway-ErrorReporter/1.0")
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8")
|
||||
conn.doOutput = true
|
||||
conn.connectTimeout = 8000
|
||||
conn.readTimeout = 8000
|
||||
OutputStreamWriter(conn.outputStream, Charsets.UTF_8).use { it.write(payload.toString()) }
|
||||
val code = conn.responseCode
|
||||
conn.disconnect()
|
||||
Log.d(TAG, "ghPost $url → HTTP $code")
|
||||
code
|
||||
} catch (e: Exception) { Log.w(TAG, "ghPost failed: ${e.message}"); -1 }
|
||||
}
|
||||
+10
@@ -77,6 +77,10 @@ class MainActivity : AppCompatActivity(), BleScaleListener, ServerEventListener
|
||||
|
||||
bleManager = BleScaleManager(this, this)
|
||||
|
||||
// Initialise error reporter early so the UncaughtExceptionHandler is installed
|
||||
// and any pending crash from a previous session is sent
|
||||
ErrorReporter.init(this)
|
||||
|
||||
deviceAdapter = DeviceAdapter(devices) { info ->
|
||||
bleManager.connect(info.device)
|
||||
}
|
||||
@@ -191,6 +195,7 @@ class MainActivity : AppCompatActivity(), BleScaleListener, ServerEventListener
|
||||
binding.tvGatewayStatus.text = "\u2705 Gateway active on port $WS_PORT"
|
||||
} catch (e: Exception) {
|
||||
binding.tvGatewayStatus.text = "\u274C Failed to start gateway: ${e.message}"
|
||||
ErrorReporter.report(e, "startGatewayServer", mapOf("port" to WS_PORT))
|
||||
}
|
||||
|
||||
// Auto-scan if there's a saved device
|
||||
@@ -287,6 +292,11 @@ class MainActivity : AppCompatActivity(), BleScaleListener, ServerEventListener
|
||||
override fun onError(message: String) {
|
||||
binding.tvScaleStatus.text = "❌ $message"
|
||||
binding.cardConnection.setCardBackgroundColor(getColor(android.R.color.holo_red_light))
|
||||
ErrorReporter.reportMessage(
|
||||
type = "ble-error",
|
||||
message = message,
|
||||
extra = mapOf("connected_device" to (bleManager.getSavedDeviceAddress() ?: "none"))
|
||||
)
|
||||
}
|
||||
|
||||
override fun onScanStopped() {
|
||||
|
||||
Reference in New Issue
Block a user