diff --git a/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessService.java b/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessService.java index 335c540..d601e5b 100644 --- a/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessService.java +++ b/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessService.java @@ -134,6 +134,7 @@ public final class RemoteAccessService extends Service { hb.put("device_id", DeviceInfo.getDeviceUuid(this)); hb.put("random", loadOrCreateSessionRandom()); hb.put("app_version", com.foxx.androidcast.BuildConfig.VERSION_NAME); + hb.put("device", buildDeviceMeta(this)); hb.put("tunnel_mode", mode.toApiValue()); JSONArray caps = new JSONArray(); if (mode == RemoteAccessMode.WIREGUARD) { @@ -353,6 +354,24 @@ public final class RemoteAccessService extends Service { nm.createNotificationChannel(ch); } + private static JSONObject buildDeviceMeta(Context context) throws Exception { + JSONObject device = new JSONObject(); + device.put("name", AppPreferences.getUsername(context)); + device.put("manufacturer", Build.MANUFACTURER); + device.put("brand", Build.BRAND); + device.put("model", Build.MODEL); + device.put("device", Build.DEVICE); + device.put("product", Build.PRODUCT); + device.put("sdk_int", Build.VERSION.SDK_INT); + device.put("release", Build.VERSION.RELEASE); + JSONArray abis = new JSONArray(); + for (String abi : Build.SUPPORTED_ABIS) { + abis.put(abi); + } + device.put("abis", abis); + return device; + } + @Override public IBinder onBind(Intent intent) { return null; diff --git a/examples/crash_reporter/backend/public/assets/css/app.css b/examples/crash_reporter/backend/public/assets/css/app.css index c22bc3f..e2d7007 100644 --- a/examples/crash_reporter/backend/public/assets/css/app.css +++ b/examples/crash_reporter/backend/public/assets/css/app.css @@ -2194,6 +2194,16 @@ button.report-tag--filter:hover { .ra-device-row--needs-wl { background: rgba(245, 158, 11, .06); } +#ra-devices-table .report-row[data-device-id] { + cursor: pointer; +} +#ra-devices-table .col-actions, +#ra-devices-table .ra-device-actions { + white-space: nowrap; +} +.reports-table--cols .col-actions { + min-width: 160px; +} .ra-device-actions { white-space: nowrap; } diff --git a/examples/crash_reporter/backend/public/assets/js/remote_access.js b/examples/crash_reporter/backend/public/assets/js/remote_access.js index 84d0ac7..fd15f1b 100644 --- a/examples/crash_reporter/backend/public/assets/js/remote_access.js +++ b/examples/crash_reporter/backend/public/assets/js/remote_access.js @@ -1,4 +1,31 @@ (function () { + const STORAGE = { + colWidths: 'ra_console_col_widths', + colOrder: 'ra_console_col_order', + }; + + const RA_COLUMNS = [ + { key: 'device_name', label: 'Device' }, + { key: 'status', label: 'Status' }, + { key: 'opt_in', label: 'Opt-in' }, + { key: 'android_api', label: 'Android API' }, + { key: 'android_version', label: 'Android version' }, + { key: 'last_seen', label: 'Last seen' }, + { key: 'app_version', label: 'App' }, + ]; + + const DEFAULT_COL_WIDTHS = { + expand: 40, + device_name: 160, + status: 120, + opt_in: 88, + android_api: 96, + android_version: 112, + last_seen: 152, + app_version: 80, + actions: 220, + }; + function basePath() { return document.body.getAttribute('data-base-path') || ''; } @@ -11,6 +38,21 @@ return document.body.getAttribute('data-can-ra-admin') === '1'; } + function lsGet(key, fallback) { + try { + const v = localStorage.getItem(key); + return v === null ? fallback : v; + } catch { + return fallback; + } + } + + function lsSet(key, value) { + try { + localStorage.setItem(key, value); + } catch { /* ignore */ } + } + function apiUrl(action, params) { const q = new URLSearchParams(params || {}); q.set('action', action); @@ -39,6 +81,247 @@ return d.innerHTML; } + function getColWidths() { + try { + const w = JSON.parse(lsGet(STORAGE.colWidths, '{}')); + return { ...DEFAULT_COL_WIDTHS, ...(w && typeof w === 'object' ? w : {}) }; + } catch { + return { ...DEFAULT_COL_WIDTHS }; + } + } + + function saveColWidths(widths) { + lsSet(STORAGE.colWidths, JSON.stringify(widths)); + } + + function getOrderedColumnKeys() { + const defaults = RA_COLUMNS.map((c) => c.key); + try { + const saved = JSON.parse(lsGet(STORAGE.colOrder, '[]')); + if (Array.isArray(saved) && saved.length) { + const out = saved.filter((k) => defaults.includes(k)); + defaults.forEach((k) => { + if (!out.includes(k)) out.push(k); + }); + return out; + } + } catch { /* ignore */ } + return defaults; + } + + function saveColumnOrder(keys) { + lsSet(STORAGE.colOrder, JSON.stringify(keys)); + } + + function getDisplayColumns() { + return getOrderedColumnKeys() + .map((k) => RA_COLUMNS.find((c) => c.key === k)) + .filter(Boolean); + } + + function columnLabelByKey(key) { + const c = RA_COLUMNS.find((col) => col.key === key); + if (c) return c.label; + if (key === 'actions') return 'Actions'; + return key; + } + + function colspan() { + return getDisplayColumns().length + 2; + } + + function applyColgroup() { + const cg = document.getElementById('ra-devices-colgroup'); + if (!cg) return; + const widths = getColWidths(); + const order = getOrderedColumnKeys(); + let html = + '
' +
+ esc(deviceTitle(d)) +
+ ''
+ );
+ case 'status':
+ return statusBadge(d);
+ case 'opt_in':
+ return esc(d.opt_in_mode || 'none');
+ case 'android_api':
+ return d.sdk_int != null && d.sdk_int !== '' ? esc(String(d.sdk_int)) : '—';
+ case 'android_version':
+ return esc(d.os_release || '—');
+ case 'last_seen':
+ return esc(d.last_seen_at || '—');
+ case 'app_version':
+ return esc(d.app_version || '—');
+ default:
+ return '—';
+ }
+ }
+
function detailLines(d) {
const lines = [];
lines.push('Device ID: ' + (d.device_id || '—'));
- if (d.device_display) {
- lines.push('Name: ' + d.device_display);
- }
+ const name = (d.device_name || d.device_display || '').trim();
+ if (name) lines.push('Device name: ' + name);
if (d.manufacturer || d.model) {
lines.push('Hardware: ' + [d.manufacturer, d.model].filter(Boolean).join(' '));
}
- if (d.os_release || d.sdk_int) {
- lines.push(
- 'Android: ' +
- [d.os_release || '', d.sdk_int ? 'SDK ' + d.sdk_int : ''].filter(Boolean).join(' · ')
- );
- }
+ if (d.brand) lines.push('Brand: ' + d.brand);
+ if (d.product) lines.push('Product: ' + d.product);
+ if (d.hardware_device) lines.push('Device codename: ' + d.hardware_device);
+ if (d.sdk_int != null) lines.push('Android API: ' + d.sdk_int);
+ if (d.os_release) lines.push('Android version: ' + d.os_release);
+ if (d.abis && d.abis.length) lines.push('ABIs: ' + formatAbis(d.abis));
lines.push('App version: ' + (d.app_version || '—'));
lines.push('Opt-in mode: ' + (d.opt_in_mode || 'none'));
lines.push('Last seen: ' + (d.last_seen_at || '—'));
- if (d.notes) {
- lines.push('Notes: ' + d.notes);
- }
- if (d.issue_count != null) {
- lines.push('Linked issues: ' + d.issue_count);
- }
- if (d.graph_session_count != null) {
- lines.push('Graph sessions: ' + d.graph_session_count);
- }
+ if (d.notes) lines.push('Notes: ' + d.notes);
+ if (d.issue_count != null) lines.push('Linked issues: ' + d.issue_count);
+ if (d.graph_session_count != null) lines.push('Graph sessions: ' + d.graph_session_count);
return lines;
}
function detailLinks(d) {
const links = d.links || [];
- if (!links.length) {
- return '';
- }
+ if (!links.length) return '';
let html = '' +
- esc(deviceTitle(d)) +
- '' + esc(line) + '
'; }); @@ -201,6 +549,12 @@ bindRaTree(tbody); } + function refreshTableChrome(devices, expandedIds) { + applyColgroup(); + renderHead(); + renderDevices(devices || lastDevices, expandedIds || lastExpanded); + } + function renderSessions(active, inactive) { const aBody = document.getElementById('ra-active-tbody'); const iBody = document.getElementById('ra-inactive-tbody'); @@ -264,11 +618,12 @@ let lastConfig = {}; async function refresh() { + const expanded = captureExpandedDeviceIds(); setStatus('Loading…'); try { const data = await fetchJson(apiUrl('dashboard')); lastConfig = data.config || {}; - renderDevices(data.devices); + refreshTableChrome(data.devices, expanded); renderSessions(data.active_sessions, data.inactive_sessions); renderEvents(data.recent_events); const ep = lastConfig.wg_endpoint ? ' · WG ' + lastConfig.wg_endpoint : ''; @@ -281,9 +636,7 @@ document.addEventListener('click', async (ev) => { const t = ev.target; if (!(t instanceof HTMLElement)) return; - if (t.closest('.report-tree-toggle, .ra-detail-links a')) { - return; - } + if (t.closest('.report-tree-toggle, .ra-detail-links a')) return; const copyId = t.getAttribute('data-copy-id'); if (copyId) { try { @@ -350,9 +703,7 @@ const form = document.getElementById('ra-whitelist-form'); if (form) { - if (!canAdmin()) { - form.hidden = true; - } + if (!canAdmin()) form.hidden = true; form.addEventListener('submit', async (ev) => { ev.preventDefault(); if (!canAdmin()) return; @@ -375,6 +726,9 @@ }); } + bindTableLayout(); + applyColgroup(); + renderHead(); refresh(); setInterval(refresh, 30000); })(); diff --git a/examples/crash_reporter/backend/src/RemoteAccessRepository.php b/examples/crash_reporter/backend/src/RemoteAccessRepository.php index 239397e..3e54856 100644 --- a/examples/crash_reporter/backend/src/RemoteAccessRepository.php +++ b/examples/crash_reporter/backend/src/RemoteAccessRepository.php @@ -24,6 +24,22 @@ final class RemoteAccessRepository { } self::ensureSessionColumns($pdo); self::ensureRsshColumns($pdo); + self::ensureDeviceMetaColumn($pdo); + } + + private static function ensureDeviceMetaColumn(PDO $pdo): void { + if (!Database::tableExists($pdo, 'remote_access_devices')) { + return; + } + $col = Database::isMysql() ? 'LONGTEXT NULL' : 'TEXT NULL'; + try { + $pdo->exec('ALTER TABLE remote_access_devices ADD COLUMN device_meta_json ' . $col); + } catch (PDOException $e) { + $msg = strtolower($e->getMessage()); + if (!str_contains($msg, 'duplicate') && !str_contains($msg, 'already exists')) { + throw $e; + } + } } /** @param list| - | Device | -Status | -Opt-in | -Last seen | -App | -Actions | -
|---|