Skip to content

Commit 65809e0

Browse files
code-rootclaude
andcommitted
feat: show Samsung/Exynos info in dashboard
- DeviceDetail overview: Samsung card with Exynos specs, Knox, One UI, AP/CSC versions - Devices list: SM-XXXXX model badge next to device name - Backend: device_model included in DeviceResponse (fetched from fingerprint) - API client: getFingerprintFull() calling /api/fingerprint/{id} with extended field - Types: Device.device_model optional field Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 58b77bf commit 65809e0

5 files changed

Lines changed: 186 additions & 38 deletions

File tree

‎backend/api/routes/devices.py‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ class DeviceResponse(BaseModel):
9999
owner_id: int
100100
created_at: datetime
101101
updated_at: datetime
102+
# populated separately from fingerprint
103+
device_model: Optional[str] = None
102104

103105
class Config:
104106
from_attributes = True
@@ -148,6 +150,15 @@ async def _log_operation(db: AsyncSession, device_id: int, user_id: int, action:
148150
await db.flush()
149151

150152

153+
def _attach_device_model(device: Device, fp_map: dict) -> DeviceResponse:
154+
"""Build DeviceResponse with device_model populated from fingerprint map."""
155+
resp = DeviceResponse.model_validate(device)
156+
fp = fp_map.get(device.id)
157+
if fp:
158+
resp.device_model = fp.device_model
159+
return resp
160+
161+
151162
@router.get("", response_model=List[DeviceResponse])
152163
async def list_devices(
153164
db: AsyncSession = Depends(get_db),
@@ -159,7 +170,15 @@ async def list_devices(
159170
result = await db.execute(
160171
select(Device).where(Device.owner_id == current_user.id).order_by(Device.created_at.desc())
161172
)
162-
return result.scalars().all()
173+
devices = result.scalars().all()
174+
if not devices:
175+
return []
176+
device_ids = [d.id for d in devices]
177+
fp_result = await db.execute(
178+
select(DeviceFingerprint).where(DeviceFingerprint.device_id.in_(device_ids))
179+
)
180+
fp_map = {fp.device_id: fp for fp in fp_result.scalars().all()}
181+
return [_attach_device_model(d, fp_map) for d in devices]
163182

164183

165184
@router.post("", response_model=DeviceResponse, status_code=status.HTTP_201_CREATED)
@@ -430,7 +449,15 @@ async def get_device(
430449
db: AsyncSession = Depends(get_db),
431450
current_user: User = Depends(get_current_user),
432451
):
433-
return await _get_device_or_404(device_id, db, current_user)
452+
device = await _get_device_or_404(device_id, db, current_user)
453+
fp_result = await db.execute(
454+
select(DeviceFingerprint).where(DeviceFingerprint.device_id == device_id)
455+
)
456+
fp = fp_result.scalar_one_or_none()
457+
resp = DeviceResponse.model_validate(device)
458+
if fp:
459+
resp.device_model = fp.device_model
460+
return resp
434461

435462

436463
@router.delete("/{device_id}", status_code=status.HTTP_204_NO_CONTENT)

‎frontend/src/api/client.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ export async function getFingerprint(id: number): Promise<DeviceFingerprint> {
146146
return res.data
147147
}
148148

149+
export async function getFingerprintFull(id: number): Promise<DeviceFingerprint & { extended: Record<string, unknown> }> {
150+
const res = await apiClient.get<DeviceFingerprint & { extended: Record<string, unknown> }>(`/api/fingerprint/${id}`)
151+
return res.data
152+
}
153+
149154
export async function updateFingerprint(
150155
id: number,
151156
data: Partial<DeviceFingerprint>

‎frontend/src/pages/DeviceDetail.tsx‎

Lines changed: 138 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
Play, Square, RotateCcw, ArrowLeft, Loader2,
66
Monitor, Fingerprint, AppWindow, Shield, Terminal, ScrollText, Info, Zap
77
} from 'lucide-react'
8-
import { getDevice, startDevice, stopDevice, restartDevice, executeShell } from '../api/client'
8+
import { getDevice, startDevice, stopDevice, restartDevice, executeShell, getFingerprintFull } from '../api/client'
99
import { useWebSocket, useDeviceH264 } from '../hooks/useWebSocket'
1010
import DeviceScreen from '../components/DeviceScreen'
1111
import FingerprintEditor from '../components/FingerprintEditor'
@@ -37,6 +37,96 @@ const TABS: { id: Tab; label: string; icon: React.ReactNode }[] = [
3737
{ id: 'logs', label: 'Logs', icon: <ScrollText className="w-4 h-4" /> },
3838
]
3939

40+
function SamsungCard({ fp }: { fp: { device_model?: string | null; manufacturer?: string | null; android_version?: string | null; sdk_version?: number | null; ap_version?: string | null; csc_version?: string | null; extended?: Record<string, unknown> } }) {
41+
const exynos = fp.extended?.exynos as { soc?: string; cores?: number; specs?: { big_cores?: { name: string; count: number; frequency_mhz: number }; little_cores?: { name: string; count: number; frequency_mhz: number }; gpu?: { model: string; frequency_mhz: number }; memory?: { total_mb: number } } } | undefined
42+
const ANDROID_TO_ONEUI: Record<string, string> = { '15': '7.0', '14': '6.0', '13': '5.1', '12': '4.1', '11': '3.1' }
43+
const oneUiStr = ANDROID_TO_ONEUI[fp.android_version || ''] || ''
44+
45+
return (
46+
<div className="card space-y-4">
47+
<div className="flex items-center gap-2 mb-1">
48+
<span className="text-blue-400 font-bold text-sm tracking-widest uppercase">Samsung</span>
49+
<span className="bg-blue-900/40 text-blue-300 text-xs px-2 py-0.5 rounded">{fp.device_model}</span>
50+
{oneUiStr && <span className="bg-indigo-900/40 text-indigo-300 text-xs px-2 py-0.5 rounded">One UI {oneUiStr}</span>}
51+
<span className="bg-green-900/40 text-green-300 text-xs px-2 py-0.5 rounded">Knox ✓</span>
52+
</div>
53+
54+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
55+
{/* Firmware info */}
56+
<div className="space-y-2">
57+
<p className="text-xs text-gray-500 uppercase tracking-wider">Firmware</p>
58+
{[
59+
['Model', fp.device_model],
60+
['Android', fp.android_version ? `Android ${fp.android_version} (API ${fp.sdk_version})` : '—'],
61+
['AP Version', fp.ap_version || '—'],
62+
['CSC Version', fp.csc_version || '—'],
63+
].map(([k, v]) => (
64+
<div key={String(k)} className="flex justify-between text-sm">
65+
<span className="text-gray-400">{k}</span>
66+
<span className="text-gray-200 font-mono text-xs">{String(v || '—')}</span>
67+
</div>
68+
))}
69+
</div>
70+
71+
{/* Exynos CPU */}
72+
<div className="space-y-2">
73+
<p className="text-xs text-gray-500 uppercase tracking-wider">Exynos Processor</p>
74+
{exynos ? (
75+
<>
76+
{[
77+
['SoC', exynos.soc],
78+
['Total Cores', exynos.cores],
79+
['Big Cores', exynos.specs?.big_cores ? `${exynos.specs.big_cores.count}x ${exynos.specs.big_cores.name} @ ${exynos.specs.big_cores.frequency_mhz} MHz` : '—'],
80+
['Little Cores', exynos.specs?.little_cores ? `${exynos.specs.little_cores.count}x ${exynos.specs.little_cores.name} @ ${exynos.specs.little_cores.frequency_mhz} MHz` : '—'],
81+
].map(([k, v]) => (
82+
<div key={String(k)} className="flex justify-between text-sm">
83+
<span className="text-gray-400">{k}</span>
84+
<span className="text-gray-200 font-mono text-xs">{String(v ?? '—')}</span>
85+
</div>
86+
))}
87+
</>
88+
) : (
89+
<p className="text-xs text-gray-600">No Exynos data — create device with firmware preset</p>
90+
)}
91+
</div>
92+
93+
{/* GPU */}
94+
<div className="space-y-2">
95+
<p className="text-xs text-gray-500 uppercase tracking-wider">GPU / Memory</p>
96+
{exynos?.specs ? (
97+
<>
98+
{[
99+
['GPU', exynos.specs.gpu ? `${exynos.specs.gpu.model} @ ${exynos.specs.gpu.frequency_mhz} MHz` : '—'],
100+
['RAM', exynos.specs.memory ? `${exynos.specs.memory.total_mb} MB` : '—'],
101+
['Knox', 'Active ✓'],
102+
['One UI', oneUiStr || '—'],
103+
].map(([k, v]) => (
104+
<div key={String(k)} className="flex justify-between text-sm">
105+
<span className="text-gray-400">{k}</span>
106+
<span className="text-gray-200 font-mono text-xs">{String(v ?? '—')}</span>
107+
</div>
108+
))}
109+
</>
110+
) : (
111+
<>
112+
{[
113+
['Knox', 'Active ✓'],
114+
['One UI', oneUiStr || '—'],
115+
['Manufacturer', fp.manufacturer || '—'],
116+
].map(([k, v]) => (
117+
<div key={String(k)} className="flex justify-between text-sm">
118+
<span className="text-gray-400">{k}</span>
119+
<span className="text-gray-200 font-mono text-xs">{String(v ?? '—')}</span>
120+
</div>
121+
))}
122+
</>
123+
)}
124+
</div>
125+
</div>
126+
</div>
127+
)
128+
}
129+
40130
function ADBConsole({ deviceId, isRunning }: { deviceId: number; isRunning: boolean }) {
41131
const [input, setInput] = useState('')
42132
const [history, setHistory] = useState<{ cmd: string; output: string; ts: number }[]>([])
@@ -119,6 +209,12 @@ export default function DeviceDetail() {
119209
refetchInterval: 10_000,
120210
})
121211

212+
const { data: fpFull } = useQuery({
213+
queryKey: ['fingerprint-full', deviceId],
214+
queryFn: () => getFingerprintFull(deviceId),
215+
enabled: !!device,
216+
})
217+
122218
const liveStatus: DeviceStatus = (currentStatus || device?.status || 'created') as DeviceStatus
123219
const isRunning = liveStatus === 'running'
124220

@@ -269,41 +365,48 @@ export default function DeviceDetail() {
269365

270366
{/* Tab Content */}
271367
{activeTab === 'overview' && (
272-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
273-
<div className="card space-y-3">
274-
<h3 className="font-semibold text-gray-200 mb-3">Device Info</h3>
275-
{[
276-
['ID', device.id],
277-
['Status', liveStatus],
278-
['AVD Name', device.avd_name || '—'],
279-
['ADB Serial', device.adb_serial || '—'],
280-
['ADB Port', device.adb_port ? `:${device.adb_port}` : '—'],
281-
['Console Port', device.console_port ? `:${device.console_port}` : '—'],
282-
['PID', device.pid || '—'],
283-
].map(([k, v]) => (
284-
<div key={String(k)} className="flex justify-between text-sm">
285-
<span className="text-gray-400">{k}</span>
286-
<span className="text-gray-200 font-mono">{String(v)}</span>
287-
</div>
288-
))}
289-
</div>
290-
<div className="card space-y-3">
291-
<h3 className="font-semibold text-gray-200 mb-3">Hardware</h3>
292-
{[
293-
['API Level', `Android API ${device.api_level}`],
294-
['Architecture', device.arch],
295-
['RAM', `${device.ram_mb} MB`],
296-
['CPU Cores', device.cpu_cores],
297-
['Owner ID', device.owner_id],
298-
['Created', new Date(device.created_at).toLocaleString()],
299-
['Updated', new Date(device.updated_at).toLocaleString()],
300-
].map(([k, v]) => (
301-
<div key={String(k)} className="flex justify-between text-sm">
302-
<span className="text-gray-400">{k}</span>
303-
<span className="text-gray-200 font-mono">{String(v)}</span>
304-
</div>
305-
))}
368+
<div className="space-y-4">
369+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
370+
<div className="card space-y-3">
371+
<h3 className="font-semibold text-gray-200 mb-3">Device Info</h3>
372+
{[
373+
['ID', device.id],
374+
['Status', liveStatus],
375+
['AVD Name', device.avd_name || '—'],
376+
['ADB Serial', device.adb_serial || '—'],
377+
['ADB Port', device.adb_port ? `:${device.adb_port}` : '—'],
378+
['Console Port', device.console_port ? `:${device.console_port}` : '—'],
379+
['PID', device.pid || '—'],
380+
].map(([k, v]) => (
381+
<div key={String(k)} className="flex justify-between text-sm">
382+
<span className="text-gray-400">{k}</span>
383+
<span className="text-gray-200 font-mono">{String(v)}</span>
384+
</div>
385+
))}
386+
</div>
387+
<div className="card space-y-3">
388+
<h3 className="font-semibold text-gray-200 mb-3">Hardware</h3>
389+
{[
390+
['API Level', `Android API ${device.api_level}`],
391+
['Architecture', device.arch],
392+
['RAM', `${device.ram_mb} MB`],
393+
['CPU Cores', device.cpu_cores],
394+
['Owner ID', device.owner_id],
395+
['Created', new Date(device.created_at).toLocaleString()],
396+
['Updated', new Date(device.updated_at).toLocaleString()],
397+
].map(([k, v]) => (
398+
<div key={String(k)} className="flex justify-between text-sm">
399+
<span className="text-gray-400">{k}</span>
400+
<span className="text-gray-200 font-mono">{String(v)}</span>
401+
</div>
402+
))}
403+
</div>
306404
</div>
405+
406+
{/* Samsung card — only shown for Samsung devices */}
407+
{fpFull?.device_model?.startsWith('SM-') && (
408+
<SamsungCard fp={fpFull} />
409+
)}
307410
</div>
308411
)}
309412

‎frontend/src/pages/Devices.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,10 +416,15 @@ export default function Devices() {
416416
<div className={clsx('w-2 h-2 rounded-full', STATUS_DOT[device.status])} />
417417
<span className="font-medium text-gray-100">{device.name}</span>
418418
{device.emulator_kind === 'physical' && (
419-
<span className="ml-2 text-[10px] uppercase px-1.5 py-0.5 rounded bg-amber-900/50 text-amber-200 border border-amber-800/60">
419+
<span className="text-[10px] uppercase px-1.5 py-0.5 rounded bg-amber-900/50 text-amber-200 border border-amber-800/60">
420420
One UI
421421
</span>
422422
)}
423+
{device.device_model?.startsWith('SM-') && (
424+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-900/50 text-blue-300 border border-blue-800/60">
425+
{device.device_model}
426+
</span>
427+
)}
423428
</div>
424429
{device.adb_serial && (
425430
<span className="text-xs text-gray-500 font-mono ml-4">{device.adb_serial}</span>

‎frontend/src/types.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ export interface Device {
1515
cpu_cores: number
1616
api_level: number
1717
arch: string
18+
/** avd = Google emulator; physical = real phone (Samsung One UI) via ADB */
19+
emulator_kind?: string
20+
/** USB/wireless serial from `adb devices` — used when emulator_kind is physical */
21+
host_adb_serial?: string | null
22+
/** Samsung model e.g. SM-G996B — populated from fingerprint */
23+
device_model?: string | null
1824
owner_id: number
1925
created_at: string
2026
updated_at: string
@@ -99,6 +105,8 @@ export interface DeviceCreateForm {
99105
preset?: string | null
100106
/** اسم ملف ZIP أو مجلد Odin تحت firmware/ (مثل G996BXXSJHZA6_G996BOXMJHZA6_XSG) */
101107
firmware_package?: string | null
108+
emulator_kind?: string
109+
host_adb_serial?: string | null
102110
}
103111

104112
export interface FirmwarePackageMeta {

0 commit comments

Comments
 (0)