mirror of
git://f0xx.org/android_cast
synced 2026-07-29 04:57:40 +03:00
be sync
This commit is contained in:
@@ -134,6 +134,7 @@ public final class RemoteAccessService extends Service {
|
|||||||
hb.put("device_id", DeviceInfo.getDeviceUuid(this));
|
hb.put("device_id", DeviceInfo.getDeviceUuid(this));
|
||||||
hb.put("random", loadOrCreateSessionRandom());
|
hb.put("random", loadOrCreateSessionRandom());
|
||||||
hb.put("app_version", com.foxx.androidcast.BuildConfig.VERSION_NAME);
|
hb.put("app_version", com.foxx.androidcast.BuildConfig.VERSION_NAME);
|
||||||
|
hb.put("device", buildDeviceMeta(this));
|
||||||
hb.put("tunnel_mode", mode.toApiValue());
|
hb.put("tunnel_mode", mode.toApiValue());
|
||||||
JSONArray caps = new JSONArray();
|
JSONArray caps = new JSONArray();
|
||||||
if (mode == RemoteAccessMode.WIREGUARD) {
|
if (mode == RemoteAccessMode.WIREGUARD) {
|
||||||
@@ -353,6 +354,24 @@ public final class RemoteAccessService extends Service {
|
|||||||
nm.createNotificationChannel(ch);
|
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
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2194,6 +2194,16 @@ button.report-tag--filter:hover {
|
|||||||
.ra-device-row--needs-wl {
|
.ra-device-row--needs-wl {
|
||||||
background: rgba(245, 158, 11, .06);
|
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 {
|
.ra-device-actions {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,31 @@
|
|||||||
(function () {
|
(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() {
|
function basePath() {
|
||||||
return document.body.getAttribute('data-base-path') || '';
|
return document.body.getAttribute('data-base-path') || '';
|
||||||
}
|
}
|
||||||
@@ -11,6 +38,21 @@
|
|||||||
return document.body.getAttribute('data-can-ra-admin') === '1';
|
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) {
|
function apiUrl(action, params) {
|
||||||
const q = new URLSearchParams(params || {});
|
const q = new URLSearchParams(params || {});
|
||||||
q.set('action', action);
|
q.set('action', action);
|
||||||
@@ -39,6 +81,247 @@
|
|||||||
return d.innerHTML;
|
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 =
|
||||||
|
'<col class="col-expand" style="width:' + (widths.expand || 40) + 'px" />';
|
||||||
|
order.forEach((k) => {
|
||||||
|
html +=
|
||||||
|
'<col data-col="' +
|
||||||
|
esc(k) +
|
||||||
|
'" style="width:' +
|
||||||
|
(widths[k] || 100) +
|
||||||
|
'px" />';
|
||||||
|
});
|
||||||
|
html +=
|
||||||
|
'<col class="col-actions" style="width:' + (widths.actions || 220) + 'px" />';
|
||||||
|
cg.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearColumnDropMarkers(thead) {
|
||||||
|
thead.querySelectorAll('.th-drop-target').forEach((el) => {
|
||||||
|
el.classList.remove('th-drop-target');
|
||||||
|
});
|
||||||
|
thead.querySelectorAll('.th-dragging').forEach((el) => {
|
||||||
|
el.classList.remove('th-dragging');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHead() {
|
||||||
|
const thead = document.getElementById('ra-devices-thead');
|
||||||
|
if (!thead) return;
|
||||||
|
const cols = getDisplayColumns();
|
||||||
|
const widths = getColWidths();
|
||||||
|
let html =
|
||||||
|
'<tr><th class="report-tree-head" aria-label="Expand"><span class="col-resizer" data-resize-col="expand"></span></th>';
|
||||||
|
cols.forEach((c) => {
|
||||||
|
const w = widths[c.key] ? ' style="width:' + widths[c.key] + 'px"' : '';
|
||||||
|
html +=
|
||||||
|
'<th class="th-draggable"' +
|
||||||
|
' draggable="true" data-col-key="' +
|
||||||
|
esc(c.key) +
|
||||||
|
'" title="Drag to reorder"' +
|
||||||
|
w +
|
||||||
|
' scope="col"><span class="th-inner">' +
|
||||||
|
'<span class="th-drag" aria-hidden="true" title="Drag column">⋮⋮</span>' +
|
||||||
|
'<span class="th-label">' +
|
||||||
|
esc(c.label) +
|
||||||
|
'</span></span>' +
|
||||||
|
'<span class="col-resizer" data-resize-col="' +
|
||||||
|
esc(c.key) +
|
||||||
|
'"></span></th>';
|
||||||
|
});
|
||||||
|
html +=
|
||||||
|
'<th class="col-actions" scope="col" style="width:' +
|
||||||
|
(widths.actions || 220) +
|
||||||
|
'px">Actions<span class="col-resizer" data-resize-col="actions"></span></th></tr>';
|
||||||
|
thead.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindTableLayout() {
|
||||||
|
const table = document.getElementById('ra-devices-table');
|
||||||
|
const thead = document.getElementById('ra-devices-thead');
|
||||||
|
if (!table || !thead || table.dataset.layoutBound === '1') return;
|
||||||
|
table.dataset.layoutBound = '1';
|
||||||
|
|
||||||
|
thead.addEventListener('dragstart', (e) => {
|
||||||
|
if (e.target.closest('.col-resizer')) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const th = e.target.closest('th.th-draggable[data-col-key]');
|
||||||
|
if (!th) return;
|
||||||
|
const sourceKey = th.getAttribute('data-col-key');
|
||||||
|
if (!sourceKey) return;
|
||||||
|
table.dataset.dragColKey = sourceKey;
|
||||||
|
th.classList.add('th-dragging');
|
||||||
|
const ghost = document.createElement('div');
|
||||||
|
ghost.className = 'col-drag-ghost';
|
||||||
|
ghost.textContent = columnLabelByKey(sourceKey);
|
||||||
|
ghost.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(ghost);
|
||||||
|
table._dragGhost = ghost;
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/plain', sourceKey);
|
||||||
|
try {
|
||||||
|
e.dataTransfer.setDragImage(ghost, 16, 14);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
thead.addEventListener('dragend', () => {
|
||||||
|
clearColumnDropMarkers(thead);
|
||||||
|
delete table.dataset.dragColKey;
|
||||||
|
if (table._dragGhost) {
|
||||||
|
table._dragGhost.remove();
|
||||||
|
table._dragGhost = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
thead.addEventListener('dragenter', (e) => {
|
||||||
|
if (!table.dataset.dragColKey) return;
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
thead.addEventListener('dragover', (e) => {
|
||||||
|
if (!table.dataset.dragColKey) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
||||||
|
const th = e.target.closest('th[data-col-key]');
|
||||||
|
if (!th) return;
|
||||||
|
const active = thead.querySelector('.th-drop-target');
|
||||||
|
if (active && active !== th) active.classList.remove('th-drop-target');
|
||||||
|
th.classList.add('th-drop-target');
|
||||||
|
});
|
||||||
|
|
||||||
|
thead.addEventListener('dragleave', (e) => {
|
||||||
|
const th = e.target.closest('th[data-col-key]');
|
||||||
|
if (!th) return;
|
||||||
|
const rel = e.relatedTarget;
|
||||||
|
if (rel && th.contains(rel)) return;
|
||||||
|
th.classList.remove('th-drop-target');
|
||||||
|
});
|
||||||
|
|
||||||
|
thead.addEventListener('drop', (e) => {
|
||||||
|
const th = e.target.closest('th[data-col-key]');
|
||||||
|
if (!th) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const sourceKey =
|
||||||
|
table.dataset.dragColKey ||
|
||||||
|
(e.dataTransfer && e.dataTransfer.getData('text/plain')) ||
|
||||||
|
'';
|
||||||
|
const targetKey = th.getAttribute('data-col-key');
|
||||||
|
clearColumnDropMarkers(thead);
|
||||||
|
delete table.dataset.dragColKey;
|
||||||
|
if (table._dragGhost) {
|
||||||
|
table._dragGhost.remove();
|
||||||
|
table._dragGhost = null;
|
||||||
|
}
|
||||||
|
if (!sourceKey || !targetKey || sourceKey === targetKey) return;
|
||||||
|
const order = getOrderedColumnKeys();
|
||||||
|
const from = order.indexOf(sourceKey);
|
||||||
|
const to = order.indexOf(targetKey);
|
||||||
|
if (from < 0 || to < 0) return;
|
||||||
|
order.splice(from, 1);
|
||||||
|
order.splice(to, 0, sourceKey);
|
||||||
|
saveColumnOrder(order);
|
||||||
|
refreshTableChrome(lastDevices, lastExpanded);
|
||||||
|
});
|
||||||
|
|
||||||
|
table.addEventListener('mousedown', (e) => {
|
||||||
|
const handle = e.target.closest('.col-resizer');
|
||||||
|
if (!handle) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const colKey = handle.getAttribute('data-resize-col');
|
||||||
|
const th = handle.closest('th');
|
||||||
|
if (!colKey || !th) return;
|
||||||
|
const widths = getColWidths();
|
||||||
|
const startX = e.clientX;
|
||||||
|
const startW = th.getBoundingClientRect().width;
|
||||||
|
|
||||||
|
const onMove = (ev) => {
|
||||||
|
const w = Math.max(48, Math.round(startW + (ev.clientX - startX)));
|
||||||
|
widths[colKey] = w;
|
||||||
|
th.style.width = w + 'px';
|
||||||
|
th.style.minWidth = w + 'px';
|
||||||
|
th.style.maxWidth = w + 'px';
|
||||||
|
const cg = document.getElementById('ra-devices-colgroup');
|
||||||
|
const col = cg && cg.querySelector('col[data-col="' + colKey + '"]');
|
||||||
|
if (col) col.style.width = w + 'px';
|
||||||
|
else if (colKey === 'actions') {
|
||||||
|
const actionCol = cg && cg.querySelector('col.col-actions');
|
||||||
|
if (actionCol) actionCol.style.width = w + 'px';
|
||||||
|
} else if (colKey === 'expand') {
|
||||||
|
const expandCol = cg && cg.querySelector('col.col-expand');
|
||||||
|
if (expandCol) expandCol.style.width = w + 'px';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onUp = () => {
|
||||||
|
document.removeEventListener('mousemove', onMove);
|
||||||
|
document.removeEventListener('mouseup', onUp);
|
||||||
|
saveColWidths(widths);
|
||||||
|
applyColgroup();
|
||||||
|
};
|
||||||
|
document.addEventListener('mousemove', onMove);
|
||||||
|
document.addEventListener('mouseup', onUp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function statusBadge(d) {
|
function statusBadge(d) {
|
||||||
const label = d.status_label || '';
|
const label = d.status_label || '';
|
||||||
if (label === 'needs_whitelist') {
|
if (label === 'needs_whitelist') {
|
||||||
@@ -57,52 +340,71 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deviceTitle(d) {
|
function deviceTitle(d) {
|
||||||
const name = (d.device_display || '').trim();
|
const name = (d.device_name || d.device_display || '').trim();
|
||||||
if (name) {
|
if (name) return name;
|
||||||
return name;
|
|
||||||
}
|
|
||||||
const id = String(d.device_id || '');
|
const id = String(d.device_id || '');
|
||||||
if (id.length > 14) {
|
if (id.length > 14) return id.slice(0, 8) + '…' + id.slice(-4);
|
||||||
return id.slice(0, 8) + '…' + id.slice(-4);
|
|
||||||
}
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAbis(abis) {
|
||||||
|
if (!Array.isArray(abis) || !abis.length) return '—';
|
||||||
|
return abis.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCell(key, d) {
|
||||||
|
switch (key) {
|
||||||
|
case 'device_name':
|
||||||
|
return (
|
||||||
|
'<code class="ra-device-id" title="' +
|
||||||
|
esc(d.device_id) +
|
||||||
|
'">' +
|
||||||
|
esc(deviceTitle(d)) +
|
||||||
|
'</code>'
|
||||||
|
);
|
||||||
|
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) {
|
function detailLines(d) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
lines.push('Device ID: ' + (d.device_id || '—'));
|
lines.push('Device ID: ' + (d.device_id || '—'));
|
||||||
if (d.device_display) {
|
const name = (d.device_name || d.device_display || '').trim();
|
||||||
lines.push('Name: ' + d.device_display);
|
if (name) lines.push('Device name: ' + name);
|
||||||
}
|
|
||||||
if (d.manufacturer || d.model) {
|
if (d.manufacturer || d.model) {
|
||||||
lines.push('Hardware: ' + [d.manufacturer, d.model].filter(Boolean).join(' '));
|
lines.push('Hardware: ' + [d.manufacturer, d.model].filter(Boolean).join(' '));
|
||||||
}
|
}
|
||||||
if (d.os_release || d.sdk_int) {
|
if (d.brand) lines.push('Brand: ' + d.brand);
|
||||||
lines.push(
|
if (d.product) lines.push('Product: ' + d.product);
|
||||||
'Android: ' +
|
if (d.hardware_device) lines.push('Device codename: ' + d.hardware_device);
|
||||||
[d.os_release || '', d.sdk_int ? 'SDK ' + d.sdk_int : ''].filter(Boolean).join(' · ')
|
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('App version: ' + (d.app_version || '—'));
|
||||||
lines.push('Opt-in mode: ' + (d.opt_in_mode || 'none'));
|
lines.push('Opt-in mode: ' + (d.opt_in_mode || 'none'));
|
||||||
lines.push('Last seen: ' + (d.last_seen_at || '—'));
|
lines.push('Last seen: ' + (d.last_seen_at || '—'));
|
||||||
if (d.notes) {
|
if (d.notes) lines.push('Notes: ' + 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.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;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detailLinks(d) {
|
function detailLinks(d) {
|
||||||
const links = d.links || [];
|
const links = d.links || [];
|
||||||
if (!links.length) {
|
if (!links.length) return '';
|
||||||
return '';
|
|
||||||
}
|
|
||||||
let html = '<div class="ra-detail-links">';
|
let html = '<div class="ra-detail-links">';
|
||||||
links.forEach((link) => {
|
links.forEach((link) => {
|
||||||
html +=
|
html +=
|
||||||
@@ -116,30 +418,71 @@
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isInteractiveTarget(el) {
|
||||||
|
return !!el.closest('.ra-device-actions, .ra-detail-links, .report-tree-toggle, button, a, input, select, textarea, label');
|
||||||
|
}
|
||||||
|
|
||||||
function bindRaTree(root) {
|
function bindRaTree(root) {
|
||||||
root.querySelectorAll('.report-tree-toggle').forEach((btn) => {
|
root.querySelectorAll('.report-row[data-device-id]').forEach((row) => {
|
||||||
btn.addEventListener('click', (e) => {
|
const btn = row.querySelector('.report-tree-toggle');
|
||||||
e.stopPropagation();
|
const briefId = btn ? btn.getAttribute('aria-controls') : null;
|
||||||
const id = btn.getAttribute('aria-controls');
|
const briefRow = briefId ? document.getElementById(briefId) : null;
|
||||||
const briefRow = id ? document.getElementById(id) : null;
|
if (!briefRow || !btn) return;
|
||||||
const row = btn.closest('.report-row');
|
|
||||||
if (!briefRow || !row) return;
|
const toggle = (e) => {
|
||||||
|
if (e && isInteractiveTarget(e.target)) return;
|
||||||
const open = briefRow.hidden;
|
const open = briefRow.hidden;
|
||||||
briefRow.hidden = !open;
|
briefRow.hidden = !open;
|
||||||
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||||
row.classList.toggle('report-row--open', open);
|
row.classList.toggle('report-row--open', open);
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggle(e);
|
||||||
|
});
|
||||||
|
row.addEventListener('click', (e) => toggle(e));
|
||||||
|
row.addEventListener('keydown', (e) => {
|
||||||
|
if (isInteractiveTarget(e.target)) return;
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
toggle(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDevices(devices) {
|
function captureExpandedDeviceIds() {
|
||||||
|
const ids = new Set();
|
||||||
|
const tbody = document.getElementById('ra-devices-tbody');
|
||||||
|
if (!tbody) return ids;
|
||||||
|
tbody.querySelectorAll('.report-row.report-row--open[data-device-id]').forEach((row) => {
|
||||||
|
const id = row.getAttribute('data-device-id');
|
||||||
|
if (id) ids.add(id);
|
||||||
|
});
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastDevices = [];
|
||||||
|
let lastExpanded = new Set();
|
||||||
|
|
||||||
|
function renderDevices(devices, expandedIds) {
|
||||||
const tbody = document.getElementById('ra-devices-tbody');
|
const tbody = document.getElementById('ra-devices-tbody');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
|
lastDevices = devices || [];
|
||||||
|
if (expandedIds) lastExpanded = expandedIds;
|
||||||
|
|
||||||
|
const cols = getDisplayColumns();
|
||||||
|
const span = colspan();
|
||||||
|
|
||||||
if (!devices || !devices.length) {
|
if (!devices || !devices.length) {
|
||||||
tbody.innerHTML =
|
tbody.innerHTML =
|
||||||
'<tr><td colspan="7" class="muted">No devices have polled yet. Enable remote access on a device (dev settings) and wait for the next poll.</td></tr>';
|
'<tr><td colspan="' +
|
||||||
|
span +
|
||||||
|
'" class="muted">No devices have polled yet. Enable remote access on a device (dev settings) and wait for the next poll.</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
devices.forEach((d, i) => {
|
devices.forEach((d, i) => {
|
||||||
const rowKey = 'ra-' + i;
|
const rowKey = 'ra-' + i;
|
||||||
@@ -148,25 +491,26 @@
|
|||||||
const canOpen = canOperate() && wl && (d.opt_in_mode === 'wireguard' || d.opt_in_mode === 'rssh');
|
const canOpen = canOperate() && wl && (d.opt_in_mode === 'wireguard' || d.opt_in_mode === 'rssh');
|
||||||
const openDisabled = canOpen ? '' : ' disabled title="Whitelist device and wait for WireGuard/RSSH opt-in poll"';
|
const openDisabled = canOpen ? '' : ' disabled title="Whitelist device and wait for WireGuard/RSSH opt-in poll"';
|
||||||
const wlDisabled = canAdmin() ? '' : ' disabled';
|
const wlDisabled = canAdmin() ? '' : ' disabled';
|
||||||
|
const isOpen = lastExpanded.has(String(d.device_id || ''));
|
||||||
const rowClass =
|
const rowClass =
|
||||||
'report-row' + (d.needs_whitelist ? ' ra-device-row--needs-wl' : '');
|
'report-row' +
|
||||||
html += '<tr class="' + rowClass + '">';
|
(d.needs_whitelist ? ' ra-device-row--needs-wl' : '') +
|
||||||
|
(isOpen ? ' report-row--open' : '');
|
||||||
|
|
||||||
|
html += '<tr class="' + rowClass + '" data-device-id="' + esc(d.device_id) + '" tabindex="0">';
|
||||||
html +=
|
html +=
|
||||||
'<td class="report-tree-cell"><button type="button" class="report-tree-toggle" aria-expanded="false" aria-controls="' +
|
'<td class="report-tree-cell"><button type="button" class="report-tree-toggle" aria-expanded="' +
|
||||||
|
(isOpen ? 'true' : 'false') +
|
||||||
|
'" aria-controls="' +
|
||||||
briefId +
|
briefId +
|
||||||
'" title="Show device details"><span class="report-tree-arrow" aria-hidden="true"></span></button></td>';
|
'" title="Show device details"><span class="report-tree-arrow" aria-hidden="true"></span></button></td>';
|
||||||
|
|
||||||
|
cols.forEach((c) => {
|
||||||
|
html += '<td data-col="' + esc(c.key) + '">' + formatCell(c.key, d) + '</td>';
|
||||||
|
});
|
||||||
|
|
||||||
html +=
|
html +=
|
||||||
'<td><code class="ra-device-id" title="' +
|
'<td class="ra-device-actions col-actions">' +
|
||||||
esc(d.device_id) +
|
|
||||||
'">' +
|
|
||||||
esc(deviceTitle(d)) +
|
|
||||||
'</code></td>';
|
|
||||||
html += '<td>' + statusBadge(d) + '</td>';
|
|
||||||
html += '<td>' + esc(d.opt_in_mode || 'none') + '</td>';
|
|
||||||
html += '<td>' + esc(d.last_seen_at || '—') + '</td>';
|
|
||||||
html += '<td>' + esc(d.app_version || '—') + '</td>';
|
|
||||||
html +=
|
|
||||||
'<td class="ra-device-actions">' +
|
|
||||||
'<button type="button" class="btn btn-sm btn-primary"' +
|
'<button type="button" class="btn btn-sm btn-primary"' +
|
||||||
openDisabled +
|
openDisabled +
|
||||||
' data-open-session="' +
|
' data-open-session="' +
|
||||||
@@ -190,7 +534,11 @@
|
|||||||
html +=
|
html +=
|
||||||
'<tr class="report-brief-row" id="' +
|
'<tr class="report-brief-row" id="' +
|
||||||
briefId +
|
briefId +
|
||||||
'" hidden><td colspan="7" class="report-brief-cell"><div class="report-brief-panel"><div class="report-brief">';
|
'"' +
|
||||||
|
(isOpen ? '' : ' hidden') +
|
||||||
|
'><td colspan="' +
|
||||||
|
span +
|
||||||
|
'" class="report-brief-cell"><div class="report-brief-panel"><div class="report-brief">';
|
||||||
detailLines(d).forEach((line) => {
|
detailLines(d).forEach((line) => {
|
||||||
html += '<p>' + esc(line) + '</p>';
|
html += '<p>' + esc(line) + '</p>';
|
||||||
});
|
});
|
||||||
@@ -201,6 +549,12 @@
|
|||||||
bindRaTree(tbody);
|
bindRaTree(tbody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshTableChrome(devices, expandedIds) {
|
||||||
|
applyColgroup();
|
||||||
|
renderHead();
|
||||||
|
renderDevices(devices || lastDevices, expandedIds || lastExpanded);
|
||||||
|
}
|
||||||
|
|
||||||
function renderSessions(active, inactive) {
|
function renderSessions(active, inactive) {
|
||||||
const aBody = document.getElementById('ra-active-tbody');
|
const aBody = document.getElementById('ra-active-tbody');
|
||||||
const iBody = document.getElementById('ra-inactive-tbody');
|
const iBody = document.getElementById('ra-inactive-tbody');
|
||||||
@@ -264,11 +618,12 @@
|
|||||||
let lastConfig = {};
|
let lastConfig = {};
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
|
const expanded = captureExpandedDeviceIds();
|
||||||
setStatus('Loading…');
|
setStatus('Loading…');
|
||||||
try {
|
try {
|
||||||
const data = await fetchJson(apiUrl('dashboard'));
|
const data = await fetchJson(apiUrl('dashboard'));
|
||||||
lastConfig = data.config || {};
|
lastConfig = data.config || {};
|
||||||
renderDevices(data.devices);
|
refreshTableChrome(data.devices, expanded);
|
||||||
renderSessions(data.active_sessions, data.inactive_sessions);
|
renderSessions(data.active_sessions, data.inactive_sessions);
|
||||||
renderEvents(data.recent_events);
|
renderEvents(data.recent_events);
|
||||||
const ep = lastConfig.wg_endpoint ? ' · WG ' + lastConfig.wg_endpoint : '';
|
const ep = lastConfig.wg_endpoint ? ' · WG ' + lastConfig.wg_endpoint : '';
|
||||||
@@ -281,9 +636,7 @@
|
|||||||
document.addEventListener('click', async (ev) => {
|
document.addEventListener('click', async (ev) => {
|
||||||
const t = ev.target;
|
const t = ev.target;
|
||||||
if (!(t instanceof HTMLElement)) return;
|
if (!(t instanceof HTMLElement)) return;
|
||||||
if (t.closest('.report-tree-toggle, .ra-detail-links a')) {
|
if (t.closest('.report-tree-toggle, .ra-detail-links a')) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const copyId = t.getAttribute('data-copy-id');
|
const copyId = t.getAttribute('data-copy-id');
|
||||||
if (copyId) {
|
if (copyId) {
|
||||||
try {
|
try {
|
||||||
@@ -350,9 +703,7 @@
|
|||||||
|
|
||||||
const form = document.getElementById('ra-whitelist-form');
|
const form = document.getElementById('ra-whitelist-form');
|
||||||
if (form) {
|
if (form) {
|
||||||
if (!canAdmin()) {
|
if (!canAdmin()) form.hidden = true;
|
||||||
form.hidden = true;
|
|
||||||
}
|
|
||||||
form.addEventListener('submit', async (ev) => {
|
form.addEventListener('submit', async (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (!canAdmin()) return;
|
if (!canAdmin()) return;
|
||||||
@@ -375,6 +726,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bindTableLayout();
|
||||||
|
applyColgroup();
|
||||||
|
renderHead();
|
||||||
refresh();
|
refresh();
|
||||||
setInterval(refresh, 30000);
|
setInterval(refresh, 30000);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -24,6 +24,22 @@ final class RemoteAccessRepository {
|
|||||||
}
|
}
|
||||||
self::ensureSessionColumns($pdo);
|
self::ensureSessionColumns($pdo);
|
||||||
self::ensureRsshColumns($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<string> $where @param list<mixed> $params */
|
/** @param list<string> $where @param list<mixed> $params */
|
||||||
@@ -152,7 +168,9 @@ final class RemoteAccessRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self::upsertDevicePoll($deviceId, $random, $appVersion, $tunnelMode, $clientIp);
|
$deviceMeta = is_array($heartbeat['device'] ?? null) ? $heartbeat['device'] : [];
|
||||||
|
|
||||||
|
self::upsertDevicePoll($deviceId, $random, $appVersion, $tunnelMode, $clientIp, $deviceMeta);
|
||||||
|
|
||||||
$device = self::findDevice($deviceId);
|
$device = self::findDevice($deviceId);
|
||||||
if (!$device || !(int) ($device['whitelisted'] ?? 0)) {
|
if (!$device || !(int) ($device['whitelisted'] ?? 0)) {
|
||||||
@@ -246,26 +264,38 @@ final class RemoteAccessRepository {
|
|||||||
string $random,
|
string $random,
|
||||||
string $appVersion,
|
string $appVersion,
|
||||||
string $tunnelMode,
|
string $tunnelMode,
|
||||||
string $clientIp
|
string $clientIp,
|
||||||
|
array $deviceMeta = []
|
||||||
): void {
|
): void {
|
||||||
$pdo = Database::pdo();
|
$pdo = Database::pdo();
|
||||||
$now = self::nowSql();
|
$now = self::nowSql();
|
||||||
|
$metaJson = self::encodeDeviceMeta($deviceMeta);
|
||||||
$existing = self::findDevice($deviceId);
|
$existing = self::findDevice($deviceId);
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
|
if ($metaJson !== null) {
|
||||||
$pdo->prepare(
|
$pdo->prepare(
|
||||||
'UPDATE remote_access_devices SET opt_in_mode = ?, last_random = ?, app_version = ?, last_seen_at = ?, updated_at = ? WHERE device_id = ?'
|
'UPDATE remote_access_devices SET opt_in_mode = ?, last_random = ?, app_version = ?,
|
||||||
|
device_meta_json = ?, last_seen_at = ?, updated_at = ? WHERE device_id = ?'
|
||||||
|
)->execute([$tunnelMode, $random, $appVersion, $metaJson, $now, $now, $deviceId]);
|
||||||
|
} else {
|
||||||
|
$pdo->prepare(
|
||||||
|
'UPDATE remote_access_devices SET opt_in_mode = ?, last_random = ?, app_version = ?,
|
||||||
|
last_seen_at = ?, updated_at = ? WHERE device_id = ?'
|
||||||
)->execute([$tunnelMode, $random, $appVersion, $now, $now, $deviceId]);
|
)->execute([$tunnelMode, $random, $appVersion, $now, $now, $deviceId]);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$pdo->prepare(
|
$pdo->prepare(
|
||||||
'INSERT INTO remote_access_devices (device_id, company_id, whitelisted, opt_in_mode, last_random, app_version, last_seen_at, created_at, updated_at)
|
'INSERT INTO remote_access_devices (device_id, company_id, whitelisted, opt_in_mode, last_random,
|
||||||
VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?)'
|
app_version, device_meta_json, last_seen_at, created_at, updated_at)
|
||||||
|
VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?, ?)'
|
||||||
)->execute([
|
)->execute([
|
||||||
$deviceId,
|
$deviceId,
|
||||||
Rbac::defaultCompanyId(),
|
Rbac::defaultCompanyId(),
|
||||||
$tunnelMode,
|
$tunnelMode,
|
||||||
$random,
|
$random,
|
||||||
$appVersion,
|
$appVersion,
|
||||||
|
$metaJson,
|
||||||
$now,
|
$now,
|
||||||
$now,
|
$now,
|
||||||
$now,
|
$now,
|
||||||
@@ -273,6 +303,37 @@ final class RemoteAccessRepository {
|
|||||||
self::logEvent($deviceId, null, null, 'device_seen', 'first_poll', ['opt_in' => $tunnelMode], $clientIp);
|
self::logEvent($deviceId, null, null, 'device_seen', 'first_poll', ['opt_in' => $tunnelMode], $clientIp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $deviceMeta */
|
||||||
|
private static function encodeDeviceMeta(array $deviceMeta): ?string {
|
||||||
|
if ($deviceMeta === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$allowed = ['name', 'manufacturer', 'brand', 'model', 'device', 'product', 'sdk_int', 'release', 'abis'];
|
||||||
|
$clean = [];
|
||||||
|
foreach ($allowed as $key) {
|
||||||
|
if (!array_key_exists($key, $deviceMeta)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$val = $deviceMeta[$key];
|
||||||
|
if ($key === 'sdk_int') {
|
||||||
|
$clean[$key] = (int) $val;
|
||||||
|
} elseif ($key === 'abis' && is_array($val)) {
|
||||||
|
$clean[$key] = array_values(array_filter(array_map('strval', $val)));
|
||||||
|
} elseif (is_scalar($val)) {
|
||||||
|
$s = trim((string) $val);
|
||||||
|
if ($s !== '') {
|
||||||
|
$clean[$key] = $s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($clean === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$json = json_encode($clean, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
|
||||||
|
return $json === false ? null : $json;
|
||||||
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed>|null */
|
/** @return array<string, mixed>|null */
|
||||||
private static function findDevice(string $deviceId): ?array {
|
private static function findDevice(string $deviceId): ?array {
|
||||||
$st = Database::pdo()->prepare('SELECT * FROM remote_access_devices WHERE device_id = ? LIMIT 1');
|
$st = Database::pdo()->prepare('SELECT * FROM remote_access_devices WHERE device_id = ? LIMIT 1');
|
||||||
@@ -753,14 +814,20 @@ final class RemoteAccessRepository {
|
|||||||
private static function enrichDeviceRow(array $row, string $issuesBase, string $projectBase): array {
|
private static function enrichDeviceRow(array $row, string $issuesBase, string $projectBase): array {
|
||||||
$deviceId = (string) ($row['device_id'] ?? '');
|
$deviceId = (string) ($row['device_id'] ?? '');
|
||||||
$companyId = (int) ($row['company_id'] ?? 1);
|
$companyId = (int) ($row['company_id'] ?? 1);
|
||||||
$ctx = self::lookupDeviceContext($deviceId, $companyId);
|
$storedMeta = self::decodeDeviceMeta($row['device_meta_json'] ?? null);
|
||||||
|
$ctx = self::lookupDeviceContext($deviceId, $companyId, $storedMeta);
|
||||||
$row['needs_whitelist'] = self::deviceNeedsWhitelist($row);
|
$row['needs_whitelist'] = self::deviceNeedsWhitelist($row);
|
||||||
$row['status_label'] = self::deviceStatusLabel($row);
|
$row['status_label'] = self::deviceStatusLabel($row);
|
||||||
|
$row['device_name'] = $ctx['device_name'] ?? '';
|
||||||
$row['device_display'] = $ctx['device_display'] ?? '';
|
$row['device_display'] = $ctx['device_display'] ?? '';
|
||||||
$row['manufacturer'] = $ctx['manufacturer'] ?? '';
|
$row['manufacturer'] = $ctx['manufacturer'] ?? '';
|
||||||
|
$row['brand'] = $ctx['brand'] ?? '';
|
||||||
$row['model'] = $ctx['model'] ?? '';
|
$row['model'] = $ctx['model'] ?? '';
|
||||||
|
$row['product'] = $ctx['product'] ?? '';
|
||||||
|
$row['hardware_device'] = $ctx['hardware_device'] ?? '';
|
||||||
$row['os_release'] = $ctx['os_release'] ?? '';
|
$row['os_release'] = $ctx['os_release'] ?? '';
|
||||||
$row['sdk_int'] = $ctx['sdk_int'] ?? null;
|
$row['sdk_int'] = $ctx['sdk_int'] ?? null;
|
||||||
|
$row['abis'] = $ctx['abis'] ?? [];
|
||||||
$row['issue_count'] = $ctx['issue_count'] ?? 0;
|
$row['issue_count'] = $ctx['issue_count'] ?? 0;
|
||||||
$row['graph_session_count'] = $ctx['graph_session_count'] ?? 0;
|
$row['graph_session_count'] = $ctx['graph_session_count'] ?? 0;
|
||||||
$row['latest_issue_id'] = $ctx['latest_issue_id'] ?? null;
|
$row['latest_issue_id'] = $ctx['latest_issue_id'] ?? null;
|
||||||
@@ -771,6 +838,16 @@ final class RemoteAccessRepository {
|
|||||||
return $row;
|
return $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private static function decodeDeviceMeta(mixed $raw): array {
|
||||||
|
if (!is_string($raw) || trim($raw) === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$decoded = json_decode($raw, true);
|
||||||
|
|
||||||
|
return is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $row */
|
/** @param array<string, mixed> $row */
|
||||||
private static function deviceNeedsWhitelist(array $row): bool {
|
private static function deviceNeedsWhitelist(array $row): bool {
|
||||||
if ((int) ($row['whitelisted'] ?? 0) === 1) {
|
if ((int) ($row['whitelisted'] ?? 0) === 1) {
|
||||||
@@ -812,23 +889,48 @@ final class RemoteAccessRepository {
|
|||||||
return (time() - $t) <= $maxAgeSec;
|
return (time() - $t) <= $maxAgeSec;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @param array<string, mixed> $storedMeta @return array<string, mixed> */
|
||||||
private static function lookupDeviceContext(string $deviceId, int $companyId): array {
|
private static function lookupDeviceContext(string $deviceId, int $companyId, array $storedMeta = []): array {
|
||||||
if ($deviceId === '') {
|
if ($deviceId === '') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
$pdo = Database::pdo();
|
$pdo = Database::pdo();
|
||||||
$like = '%' . $deviceId . '%';
|
$deviceName = trim((string) ($storedMeta['name'] ?? ''));
|
||||||
$issueCount = 0;
|
$manufacturer = trim((string) ($storedMeta['manufacturer'] ?? ''));
|
||||||
$latestIssueId = null;
|
$brand = trim((string) ($storedMeta['brand'] ?? ''));
|
||||||
$deviceDisplay = '';
|
$model = trim((string) ($storedMeta['model'] ?? ''));
|
||||||
$manufacturer = '';
|
$hardwareDevice = trim((string) ($storedMeta['device'] ?? ''));
|
||||||
$model = '';
|
$product = trim((string) ($storedMeta['product'] ?? ''));
|
||||||
$osRelease = '';
|
$osRelease = trim((string) ($storedMeta['release'] ?? ''));
|
||||||
$sdkInt = null;
|
$sdkInt = isset($storedMeta['sdk_int']) ? (int) $storedMeta['sdk_int'] : null;
|
||||||
|
$abis = is_array($storedMeta['abis'] ?? null) ? $storedMeta['abis'] : [];
|
||||||
$appVersion = '';
|
$appVersion = '';
|
||||||
|
|
||||||
|
$issueCount = 0;
|
||||||
|
$latestIssueId = null;
|
||||||
|
$graphCount = 0;
|
||||||
|
|
||||||
|
if (Database::tableExists($pdo, 'graph_sessions')) {
|
||||||
|
$gSt = $pdo->prepare(
|
||||||
|
'SELECT app_version, sdk_int FROM graph_sessions WHERE device_id = ? ORDER BY started_at_ms DESC LIMIT 1'
|
||||||
|
);
|
||||||
|
$gSt->execute([$deviceId]);
|
||||||
|
$gRow = $gSt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (is_array($gRow)) {
|
||||||
|
if ($appVersion === '') {
|
||||||
|
$appVersion = trim((string) ($gRow['app_version'] ?? ''));
|
||||||
|
}
|
||||||
|
if ($sdkInt === null && isset($gRow['sdk_int'])) {
|
||||||
|
$sdkInt = (int) $gRow['sdk_int'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$cntSt = $pdo->prepare('SELECT COUNT(*) FROM graph_sessions WHERE device_id = ?');
|
||||||
|
$cntSt->execute([$deviceId]);
|
||||||
|
$graphCount = (int) $cntSt->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
if (Database::tableExists($pdo, 'reports')) {
|
if (Database::tableExists($pdo, 'reports')) {
|
||||||
|
$like = '%' . $deviceId . '%';
|
||||||
$cntSt = $pdo->prepare(
|
$cntSt = $pdo->prepare(
|
||||||
'SELECT COUNT(*) FROM reports r WHERE r.company_id = ? AND r.payload_json LIKE ?'
|
'SELECT COUNT(*) FROM reports r WHERE r.company_id = ? AND r.payload_json LIKE ?'
|
||||||
);
|
);
|
||||||
@@ -845,15 +947,36 @@ final class RemoteAccessRepository {
|
|||||||
$latest = $latestSt->fetch(PDO::FETCH_ASSOC);
|
$latest = $latestSt->fetch(PDO::FETCH_ASSOC);
|
||||||
if (is_array($latest)) {
|
if (is_array($latest)) {
|
||||||
$latestIssueId = (int) ($latest['id'] ?? 0) ?: null;
|
$latestIssueId = (int) ($latest['id'] ?? 0) ?: null;
|
||||||
$appVersion = (string) ($latest['app_version'] ?? '');
|
if ($appVersion === '') {
|
||||||
|
$appVersion = trim((string) ($latest['app_version'] ?? ''));
|
||||||
|
}
|
||||||
$payload = json_decode((string) ($latest['payload_json'] ?? '{}'), true);
|
$payload = json_decode((string) ($latest['payload_json'] ?? '{}'), true);
|
||||||
if (is_array($payload)) {
|
if (is_array($payload)) {
|
||||||
$device = is_array($payload['device'] ?? null) ? $payload['device'] : [];
|
$device = is_array($payload['device'] ?? null) ? $payload['device'] : [];
|
||||||
|
if ($manufacturer === '') {
|
||||||
$manufacturer = trim((string) ($device['manufacturer'] ?? ''));
|
$manufacturer = trim((string) ($device['manufacturer'] ?? ''));
|
||||||
|
}
|
||||||
|
if ($brand === '') {
|
||||||
|
$brand = trim((string) ($device['brand'] ?? ''));
|
||||||
|
}
|
||||||
|
if ($model === '') {
|
||||||
$model = trim((string) ($device['model'] ?? $latest['device_model'] ?? ''));
|
$model = trim((string) ($device['model'] ?? $latest['device_model'] ?? ''));
|
||||||
|
}
|
||||||
|
if ($hardwareDevice === '') {
|
||||||
|
$hardwareDevice = trim((string) ($device['device'] ?? ''));
|
||||||
|
}
|
||||||
|
if ($product === '') {
|
||||||
|
$product = trim((string) ($device['product'] ?? ''));
|
||||||
|
}
|
||||||
|
if ($osRelease === '') {
|
||||||
$osRelease = trim((string) ($device['release'] ?? ''));
|
$osRelease = trim((string) ($device['release'] ?? ''));
|
||||||
$sdkInt = isset($device['sdk_int']) ? (int) $device['sdk_int'] : null;
|
}
|
||||||
$deviceDisplay = trim($manufacturer . ' ' . $model);
|
if ($sdkInt === null && isset($device['sdk_int'])) {
|
||||||
|
$sdkInt = (int) $device['sdk_int'];
|
||||||
|
}
|
||||||
|
if ($abis === [] && is_array($device['abis'] ?? null)) {
|
||||||
|
$abis = $device['abis'];
|
||||||
|
}
|
||||||
if ($appVersion === '' && is_array($payload['app'] ?? null)) {
|
if ($appVersion === '' && is_array($payload['app'] ?? null)) {
|
||||||
$appVersion = trim((string) ($payload['app']['version_name'] ?? ''));
|
$appVersion = trim((string) ($payload['app']['version_name'] ?? ''));
|
||||||
}
|
}
|
||||||
@@ -861,22 +984,24 @@ final class RemoteAccessRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$graphCount = 0;
|
$deviceDisplay = $deviceName !== ''
|
||||||
if (Database::tableExists($pdo, 'graph_sessions')) {
|
? $deviceName
|
||||||
$gSt = $pdo->prepare('SELECT COUNT(*) FROM graph_sessions WHERE device_id = ?');
|
: trim($manufacturer . ' ' . $model);
|
||||||
$gSt->execute([$deviceId]);
|
|
||||||
$graphCount = (int) $gSt->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'issue_count' => $issueCount,
|
'issue_count' => $issueCount,
|
||||||
'latest_issue_id' => $latestIssueId,
|
'latest_issue_id' => $latestIssueId,
|
||||||
'graph_session_count' => $graphCount,
|
'graph_session_count' => $graphCount,
|
||||||
|
'device_name' => $deviceName,
|
||||||
'device_display' => $deviceDisplay,
|
'device_display' => $deviceDisplay,
|
||||||
'manufacturer' => $manufacturer,
|
'manufacturer' => $manufacturer,
|
||||||
|
'brand' => $brand,
|
||||||
'model' => $model,
|
'model' => $model,
|
||||||
|
'hardware_device' => $hardwareDevice,
|
||||||
|
'product' => $product,
|
||||||
'os_release' => $osRelease,
|
'os_release' => $osRelease,
|
||||||
'sdk_int' => $sdkInt,
|
'sdk_int' => $sdkInt,
|
||||||
|
'abis' => $abis,
|
||||||
'app_version' => $appVersion,
|
'app_version' => $appVersion,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -446,17 +446,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="reports-table-wrap">
|
<div class="reports-table-wrap">
|
||||||
<table class="data-table reports-tree reports-table--cols" id="ra-devices-table">
|
<table class="data-table reports-tree reports-table--cols" id="ra-devices-table">
|
||||||
<thead>
|
<colgroup id="ra-devices-colgroup"></colgroup>
|
||||||
<tr>
|
<thead id="ra-devices-thead"></thead>
|
||||||
<th class="report-tree-head" aria-label="Expand"></th>
|
|
||||||
<th scope="col">Device</th>
|
|
||||||
<th scope="col">Status</th>
|
|
||||||
<th scope="col">Opt-in</th>
|
|
||||||
<th scope="col">Last seen</th>
|
|
||||||
<th scope="col">App</th>
|
|
||||||
<th scope="col">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="ra-devices-tbody"></tbody>
|
<tbody id="ra-devices-tbody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user