diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts
index 340057a..d9bd9c2 100644
--- a/apps/desktop/electron/main.ts
+++ b/apps/desktop/electron/main.ts
@@ -9,6 +9,7 @@ import tls from 'node:tls'
import { pathToFileURL, fileURLToPath } from 'node:url'
import { startChromeBridge } from './chrome-bridge.mjs'
import { createMobileWindow } from './mobile-host.mjs'
+import { MobileVault } from './mobile-secrets.mjs'
import {
app,
@@ -34,6 +35,14 @@ import {
import { classifyActiveRuntime, needsPackagedRuntimeUpgrade } from './active-runtime-state'
import { createAiturkCliRunner, createAiturkLogin } from './aiturk-login'
+import {
+ createMobileLink,
+ createMobileLinkFileStore,
+ createWindowsFirewall,
+ defaultRouteAddress,
+ type MobileLink,
+ selectLanAddresses
+} from './mobile-lan-proxy'
import { describeVcRuntimeResult, ensureWindowsVcRuntime } from './windows-vc-runtime'
import { destroyKeepaliveAgents, downloadAgentFor, jsonAgentFor, withRetry } from './api-transport'
import { appIconCandidates, resolveAppIcon } from './app-icon'
@@ -17818,19 +17827,90 @@ function openMobileConnectionWindow() {
return
}
mobileConnectionWindow = createMobileWindow({
- directory: path.join(app.getPath('userData'), 'aiturk-mobile'),
- assets: path.dirname(fileURLToPath(import.meta.url)),
- resolveBackend: async () => {
- const profile = primaryProfileKey()
- if (globalRemoteActive() || profileHasRemoteOverride(profile)) throw new Error('Mobil bağlantı için yerel IDE bağlantısını seçin.')
- return ensureBackend(profile)
- }
- })
+ assets: path.dirname(fileURLToPath(import.meta.url)),
+ link: getMobileLink()
+ })
}
+// "Telefon bağlantısı" (electron/mobile-lan-proxy.ts): TLS LAN proxy in front of
+// the loopback backend + pairing. One controller for the whole app run; it only
+// listens while the feature is on and a phone is paired or being paired.
+let mobileLink: MobileLink | null = null
+
+function mobileLinkRemoteMode() {
+ return globalRemoteActive() || profileHasRemoteOverride(primaryProfileKey())
+}
+
+function getMobileLink(): MobileLink {
+ if (mobileLink) {
+ return mobileLink
+ }
+
+ const directory = path.join(app.getPath('userData'), 'aiturk-mobile', 'mobile')
+
+ mobileLink = createMobileLink({
+ // Same vault file as before (mobile-secrets.bin, safeStorage); older fields are kept.
+ vault: new MobileVault(directory, safeStorage),
+ store: createMobileLinkFileStore(directory, path.join(directory, 'lan-link.json'), fs.promises),
+ resolveBackend: async () => {
+ if (mobileLinkRemoteMode()) {
+ throw new Error('aiturk-mobile:remote_mode')
+ }
+
+ const connection = await ensureBackend(primaryProfileKey())
+
+ if (connection.mode !== 'local') {
+ throw new Error('aiturk-mobile:remote_mode')
+ }
+
+ return { baseUrl: connection.baseUrl, token: connection.token }
+ },
+ isRemoteMode: mobileLinkRemoteMode,
+ firewall: createWindowsFirewall({ spawn, platform: process.platform, env: process.env }),
+ fetch: (url, init) => fetch(url, init as RequestInit),
+ addresses: async () => selectLanAddresses(os.networkInterfaces(), await defaultRouteAddress()),
+ version: app.getVersion(),
+ pcName: os.hostname(),
+ platform: process.platform,
+ // Method + path + status only (no token, code, query or body). Successful
+ // API traffic is not logged; pairing and refusals are, for diagnosis.
+ log: line => {
+ if (/ \/aiturk\//.test(line) || / [45]\d\d$/.test(line)) {
+ rememberLog(`[mobile-lan] ${line}`)
+ }
+ }
+ })
+
+ return mobileLink
+}
+
+// Only the primary window's top-level frame may open the phone window.
+ipcMain.handle('aiturk:mobile:open', async event => {
+ assertAiturkLoginSender(event)
+
+ if (process.platform !== 'win32') {
+ throw new Error('aiturk-mobile:unsupported')
+ }
+
+ openMobileConnectionWindow()
+
+ return { ok: true }
+})
+
+app.on('will-quit', () => {
+ void mobileLink?.stop().catch(() => undefined)
+})
+
app.whenReady().then(() => {
// Explicit development/beta entry point; never enables remote access on boot.
if (process.platform === 'win32' && process.argv.includes('--aiturk-mobile')) openMobileConnectionWindow()
+ // Resume the phone link only when the user turned it on earlier AND a phone
+ // is paired; otherwise nothing listens (and no vault / UAC prompt is touched).
+ if (process.platform === 'win32') {
+ void getMobileLink()
+ .start()
+ .catch(error => rememberLog(`[mobile-lan] start failed: ${error instanceof Error ? error.message : 'error'}`))
+ }
if (process.platform === 'win32' && process.env.LOCALAPPDATA) {
void startChromeBridge({
directory: path.join(process.env.LOCALAPPDATA, 'TurkServis', 'AITURK-IDE'),
diff --git a/apps/desktop/electron/mobile-host-ui.mjs b/apps/desktop/electron/mobile-host-ui.mjs
index 84b2b87..c039b31 100644
--- a/apps/desktop/electron/mobile-host-ui.mjs
+++ b/apps/desktop/electron/mobile-host-ui.mjs
@@ -1,94 +1,135 @@
-import { completeIce, rtcConfiguration, sendFrame } from './mobile/transport.mjs'
-import { drawQr, InviteScanner } from './mobile/qr.mjs'
-import { createDeviceIdentity, loadDeviceIdentity, assertRemoteIdentity, parsePhoneIdentity } from './mobile/peer-identity.mjs'
-const host = window.mobileHost, $ = id => document.getElementById(id), peers = new Map()
-const status = text => { $('status').textContent = text }
-function action(id, work) { $(id).onclick = async () => { $(id).disabled = true; try { await work() } catch (error) { status(error.message) } finally { $(id).disabled = false } } }
-const identityScanner = new InviteScanner(document.createElement('video'), async raw => {
- if (raw.length > 700) throw new Error('Telefon kimlik belgesi çok büyük.')
- const value = JSON.parse(raw)
- const pairingId = $('pairing').value.trim() || value.pairingId
- parsePhoneIdentity(raw, pairingId)
- if (!/^[0-9a-f-]{36}$/.test(pairingId)) throw new Error('Eşleştirme kimliği geçersiz.')
- $('pairing').value = pairingId; $('phone-identity').value = raw
- status('Telefon kimliği QR resminden alındı. Telefondaki kısa kodu girin ve bu telefonu ayrıca onaylayın; henüz izin verilmedi.')
-})
-$('phone-identity-file').onchange = async event => {
- try { await identityScanner.image(event.target.files[0]) }
- catch (error) { status(error.message) }
- finally { event.target.value = '' }
+// "Telefon bağlantısı" window UI. Talks to electron/mobile-host.mjs over the
+// `mobileHost` bridge (mobile-preload.cjs); all logic lives in the main process.
+/* global window, document */
+import { drawQr } from './mobile/qr.mjs'
+
+const TEXT = {
+ tr: {
+ eyebrow: 'AITURK · BİLGİSAYAR', title: 'Telefon bağlantısı',
+ intro: 'AITURK Mobil uygulaması, aynı Wi-Fi ağındayken bu bilgisayardaki IDE konuşmalarınızı görür ve sohbet eder. Hesap girişi gerekmez.',
+ enabled: 'Telefon bağlantısı açık', disabled: 'Telefon bağlantısı kapalı',
+ remote: 'IDE şu an uzak bir ağ geçidine bağlı. Telefon bağlantısı yalnız yerel IDE bağlantısıyla çalışır; Ayarlar’dan yerel bağlantıyı seçin.',
+ firewallNeeded: 'Windows Güvenlik Duvarı izni henüz verilmedi. İzin olmadan telefon bu bilgisayara ulaşamayabilir.',
+ firewallDenied: 'Windows izin penceresi reddedildi; Güvenlik Duvarı kuralı eklenemedi. İzin olmadan telefon bu bilgisayara ulaşamayabilir.',
+ firewallFailed: 'Windows Güvenlik Duvarı kuralı eklenemedi. İzin olmadan telefon bu bilgisayara ulaşamayabilir.',
+ retry: 'Yeniden dene', addPhone: 'Telefon ekle', cancel: 'Vazgeç',
+ scan: 'Telefonda AITURK Mobil → “Bilgisayara bağlan” → bu QR kodunu okutun.',
+ orCode: 'ya da telefonda şu kodu yazın:',
+ codePending: 'Kodla bağlanma hazırlanıyor…', codeReady: 'Kodla bağlanma hazır.',
+ codeUnavailable: 'Kodla bağlanma şu an kullanılamıyor; QR kodunu okutun.',
+ sameWifi: 'Telefon ve bilgisayar aynı Wi-Fi ağında olmalı.',
+ countdown: (m, s) => `Kod ${m}:${s} sonra geçersiz olur`, expired: 'Kodun süresi doldu. Yeniden “Telefon ekle” seçin.',
+ devices: 'Bağlı telefonlar', noDevices: 'Henüz bağlı telefon yok.',
+ added: 'Eklenme', lastSeen: 'Son görülme', online: 'şu an bağlı', remove: 'Kaldır',
+ removed: name => `${name} kaldırıldı.`, paired: name => `${name} bağlandı.`,
+ footer: port => `Bağlantı noktası: ${port}`, removeRule: 'Güvenlik Duvarı kuralını kaldır', ruleRemoved: 'Güvenlik Duvarı kuralı kaldırıldı.',
+ errors: {
+ remote_mode: 'IDE uzak ağ geçidine bağlıyken telefon bağlantısı açılamaz.',
+ vault_unavailable: 'Windows güvenli anahtar kasası kullanılamıyor; telefon bağlantısı açılamadı.',
+ not_enabled: 'Önce telefon bağlantısını açın.',
+ no_lan_address: 'Bu bilgisayar bir yerel ağa (Wi-Fi ya da kablo) bağlı görünmüyor.',
+ port_unavailable: 'Boş bağlantı noktası bulunamadı (47321–47340 kullanımda).',
+ failed: 'İşlem tamamlanamadı. Yeniden deneyin.',
+ },
+ },
+ en: {
+ eyebrow: 'AITURK · COMPUTER', title: 'Phone connection',
+ intro: 'The AITURK Mobile app can see and continue this computer’s IDE conversations while on the same Wi-Fi network. No account sign-in is needed.',
+ enabled: 'Phone connection is on', disabled: 'Phone connection is off',
+ remote: 'The IDE is connected to a remote gateway. Phone connection only works with the local IDE connection; choose the local connection in Settings.',
+ firewallNeeded: 'Windows Firewall permission has not been granted yet. Without it the phone may not reach this computer.',
+ firewallDenied: 'The Windows permission prompt was declined, so the Firewall rule was not added. Without it the phone may not reach this computer.',
+ firewallFailed: 'The Windows Firewall rule could not be added. Without it the phone may not reach this computer.',
+ retry: 'Try again', addPhone: 'Add a phone', cancel: 'Cancel',
+ scan: 'On the phone: AITURK Mobile → “Connect to computer” → scan this QR code.',
+ orCode: 'or type this code on the phone:',
+ codePending: 'Preparing code sign-in…', codeReady: 'Connecting with the code is ready.',
+ codeUnavailable: 'Connecting with the code is unavailable right now; scan the QR code.',
+ sameWifi: 'The phone and the computer must be on the same Wi-Fi network.',
+ countdown: (m, s) => `The code expires in ${m}:${s}`, expired: 'The code expired. Choose “Add a phone” again.',
+ devices: 'Connected phones', noDevices: 'No phone is connected yet.',
+ added: 'Added', lastSeen: 'Last seen', online: 'connected now', remove: 'Remove',
+ removed: name => `${name} was removed.`, paired: name => `${name} connected.`,
+ footer: port => `Port: ${port}`, removeRule: 'Remove the Firewall rule', ruleRemoved: 'The Firewall rule was removed.',
+ errors: {
+ remote_mode: 'Phone connection cannot be turned on while the IDE uses a remote gateway.',
+ vault_unavailable: 'The Windows secure key store is unavailable; phone connection could not be turned on.',
+ not_enabled: 'Turn phone connection on first.',
+ no_lan_address: 'This computer does not seem to be on a local network (Wi-Fi or cable).',
+ port_unavailable: 'No free port was found (47321–47340 are in use).',
+ failed: 'That did not work. Try again.',
+ },
+ },
}
-action('login-start', async () => { const result = await host.call('account.login.start'); $('login-code').textContent = `Açılan hesap sayfasında yalnız bu kodu onaylayın: ${result.code}` })
-action('login-check', async () => { const result = await host.call('account.login.check'); status(result.approved ? 'Hesaba giriş yapıldı. Bu bilgisayarı eşleştirebilirsiniz.' : 'Tarayıcıdaki kod onayı bekleniyor.') })
-action('logout', async () => { await host.call('account.logout'); listening = false; $('login-code').textContent = ''; status('Mobil oturum kapatıldı; hesap ve Google anahtarı bu bilgisayar kasasından temizlendi.') })
-async function desktopIdentity(create = false) {
- const setup = await host.call('setup')
- if (!setup.deviceId) throw new Error('Önce bu bilgisayarı hesapla eşleştirin.')
- if (setup.localFingerprint) return loadDeviceIdentity(setup.deviceId, setup.localFingerprint)
- if (!create) throw new Error('Önce bu bilgisayarda yeni telefon QR kodu oluşturun.')
- const identity = await createDeviceIdentity(setup.deviceId)
- await host.call('identity.save', { deviceId: setup.deviceId, fingerprint: identity.fingerprint })
- return identity
+const lang = (navigator.language || '').toLowerCase().startsWith('en') ? 'en' : 'tr'
+const t = TEXT[lang], host = window.mobileHost, $ = id => document.getElementById(id)
+const when = value => new Date(value).toLocaleString(lang === 'tr' ? 'tr-TR' : 'en-GB', { dateStyle: 'medium', timeStyle: 'short' })
+document.documentElement.lang = lang; document.title = `AITURK · ${t.title}`
+for (const [id, key] of [['t-eyebrow', 'eyebrow'], ['t-title', 'title'], ['t-intro', 'intro'], ['t-remote', 'remote'], ['t-scan', 'scan'],
+ ['t-or-code', 'orCode'], ['t-same-wifi', 'sameWifi'], ['t-devices', 'devices'], ['devices-empty', 'noDevices'], ['firewall-retry', 'retry'],
+ ['pair-begin', 'addPhone'], ['pair-cancel', 'cancel'], ['firewall-remove', 'removeRule']]) $(id).textContent = t[key]
+
+let current = null, drawnQr = '', busy = false
+function notice(text) { $('notice').textContent = text || ''; $('notice').hidden = !text }
+const errorText = error => t.errors[/aiturk-mobile:([a-z_]+)/.exec(String(error?.message))?.[1]] || t.errors.failed
+
+function tick() {
+ const pairing = current?.pairing
+ if (!pairing) return
+ const left = Math.max(0, Math.ceil((pairing.expiresAt - Date.now()) / 1000))
+ $('pair-countdown').textContent = t.countdown(Math.floor(left / 60), String(left % 60).padStart(2, '0'))
+ if (left === 0) { notice(t.expired); void refresh() }
}
-action('invite', async () => {
- const identity = await desktopIdentity(true), invite = await host.call('invite.create')
- drawQr($('invite-qr'), JSON.stringify({ ...invite, version: 2, desktopFingerprint: identity.fingerprint }))
- $('invite-qr').hidden = false
- $('invite-status').textContent = `Telefonda aynı hesaba giriş yapıp bu QR kodunu tarayın. Son geçerlilik: ${invite.expiresAt}`
-})
-action('invite-check', async () => { const result = await host.call('invite.check'); if (!result.pending) { status('QR ile gelen telefon henüz yok.'); return }; $('pairing').value = result.pending.pairingId; status(`${result.pending.name} bulundu. Telefondaki kısa kodu girip “Bu telefonu onayla” seçin.`) })
-action('save', async () => { const token = $('account').value; $('account').value = ''; await host.call('account.save', { token }); status('Hesap anahtarı Windows kasasına kaydedildi.') })
-action('pair', async () => { const pair = await host.call('desktop.pair'); $('code').textContent = `Açılan hesap sayfasında bu kodu onaylayın: ${pair.code}. Süre: ${pair.expiresAt}` })
-action('approve', async () => { await host.call('phone.approve', { pairingId: $('pairing').value.trim(), code: $('phone-code').value.trim().toUpperCase(), identity: $('phone-identity').value.trim() }); $('phone-code').value = ''; $('phone-identity').value = ''; status('Telefon bu bilgisayarda kimliğiyle onaylandı.') })
-action('refresh', async () => {
- const data = await host.call('devices'); $('devices').replaceChildren()
- for (const device of data.devices) {
- const row = document.createElement('p'), label = document.createElement('span'), button = document.createElement('button')
- label.textContent = `${device.name} · ${device.platform}${device.self ? ' · Bu bilgisayar' : ''}`
- button.textContent = 'Cihazı iptal et'
- button.onclick = async () => { button.disabled = true; try { await host.call('device.revoke', { deviceId: device.id, version: device.version }); row.remove(); status('Cihaz iptal edildi.') } catch (error) { status(error.message); button.disabled = false } }
- row.append(label, button); $('devices').append(row)
- }
-})
-function close(id) { const entry = peers.get(id); if (!entry) return; peers.delete(id); clearInterval(entry.timer); entry.pc.close(); void host.call('channel.close', { channelId: id }) }
-host.onEvent(event => { if (event.close) { close(event.channelId); return }; const entry = peers.get(event.channelId); if (entry?.channel?.readyState === 'open') sendFrame(entry.channel, event.frame) })
-async function connect(offer) {
- assertRemoteIdentity(offer.sdp, offer.trustedPhoneFingerprint)
- const ice = await host.call('ice')
- const identity = await desktopIdentity()
- const pc = new RTCPeerConnection({ ...rtcConfiguration(ice.iceServers), certificates: [identity.certificate] }), entry = { pc }; peers.set(offer.id, entry)
- pc.ondatachannel = event => {
- const channel = event.channel; entry.channel = channel
- if (channel.label !== 'aiturk-mobile-v1') { close(offer.id); return }
- let pending = 0
- channel.onmessage = async event => {
- if (typeof event.data !== 'string' || event.data.length > 65000 || ++pending > 8) { close(offer.id); return }
- let message
- try {
- message = JSON.parse(event.data)
- const result = await host.call('channel.request', { channelId: offer.id, message })
- if (channel.readyState === 'open') sendFrame(channel, { id: message.id, result })
- } catch (error) { if (message?.id && channel.readyState === 'open') channel.send(JSON.stringify({ id: message.id, error: error.message })) }
- finally { pending-- }
- }
- channel.onclose = () => close(offer.id)
- channel.onopen = () => status('Telefon şifreli veri kanalıyla bağlı.')
- }
- try {
- await pc.setRemoteDescription({ type: 'offer', sdp: offer.sdp })
- await pc.setLocalDescription(await pc.createAnswer()); await completeIce(pc)
- await host.call('channel.answer', { channelId: offer.id, sdp: pc.localDescription.sdp })
- entry.timer = setInterval(() => { void host.call('channel.heartbeat', { channelId: offer.id }).catch(() => close(offer.id)) }, 10000)
- } catch (error) { close(offer.id); throw error }
+
+function render(state) {
+ current = state
+ $('enabled').checked = state.enabled; $('t-enabled').textContent = state.enabled ? t.enabled : t.disabled
+ $('remote-box').hidden = !state.remote
+ const firewall = { needed: t.firewallNeeded, denied: t.firewallDenied, failed: t.firewallFailed }[state.firewall]
+ $('firewall-box').hidden = !(state.enabled && firewall); $('firewall-text').textContent = firewall || ''
+ $('pair-section').hidden = !state.enabled || state.remote
+ $('pair-begin').hidden = Boolean(state.pairing); $('pair-live').hidden = !state.pairing
+ if (state.pairing) {
+ if (drawnQr !== state.pairing.qr) { drawQr($('pair-qr'), state.pairing.qr); drawnQr = state.pairing.qr }
+ $('pair-qr').setAttribute('aria-label', t.scan)
+ $('pair-code').textContent = `${state.pairing.code.slice(0, 4)}-${state.pairing.code.slice(4)}`
+ $('pair-code-path').textContent = { pending: t.codePending, ready: t.codeReady, unavailable: t.codeUnavailable }[state.pairing.codePath]
+ tick()
+ } else { drawnQr = ''; $('pair-code').textContent = '' }
+ $('devices-section').hidden = !state.enabled && state.devices.length === 0
+ $('devices-empty').hidden = state.devices.length > 0
+ $('devices').replaceChildren(...state.devices.map(device => {
+ const row = document.createElement('div'), label = document.createElement('div'), name = document.createElement('strong')
+ const added = document.createElement('small'), seen = document.createElement('small'), button = document.createElement('button')
+ row.className = 'device'; name.textContent = device.name
+ added.textContent = `${t.added}: ${when(device.createdAt)}`
+ seen.textContent = device.live > 0 ? t.online : `${t.lastSeen}: ${when(device.lastSeenAt)}`
+ button.textContent = t.remove; button.className = 'danger'
+ button.onclick = () => run(async () => { render(await host.call('device.revoke', { deviceId: device.id })); notice(t.removed(device.name)) })
+ label.append(name, added, seen); row.append(label, button)
+ return row
+ }))
+ $('footer').hidden = state.port === null
+ $('footer-info').textContent = state.port === null ? '' : t.footer(state.port)
+ $('firewall-remove').hidden = state.firewall !== 'ok'
}
-let listening = false
-action('listen', async () => {
- if (listening) return
- listening = true; status('Onaylı telefon bağlantısı bekleniyor.')
- while (listening) {
- try { const data = await host.call('inbox'); for (const offer of data.channels) if (!peers.has(offer.id)) await connect(offer) }
- catch (error) { status(error.message); listening = false; break }
- await new Promise(resolve => setTimeout(resolve, 2000))
- }
-})
-window.addEventListener('pagehide', () => { listening = false; for (const id of peers.keys()) close(id) })
+
+async function run(work) {
+ if (busy) return
+ busy = true; document.body.setAttribute('aria-busy', 'true')
+ for (const button of document.querySelectorAll('button,input')) button.disabled = true
+ try { await work() } catch (error) { notice(errorText(error)); await refresh() }
+ finally { busy = false; document.body.removeAttribute('aria-busy'); for (const button of document.querySelectorAll('button,input')) button.disabled = false }
+}
+async function refresh() { try { render(await host.call('state')) } catch (error) { notice(errorText(error)) } }
+
+$('enabled').onchange = () => { const on = $('enabled').checked; void run(async () => { notice(''); render(await host.call(on ? 'enable' : 'disable')) }) }
+$('pair-begin').onclick = () => run(async () => { notice(''); render(await host.call('pair.begin')) })
+$('pair-cancel').onclick = () => run(async () => { render(await host.call('pair.cancel')) })
+$('firewall-retry').onclick = () => run(async () => { notice(''); render(await host.call('firewall.retry')) })
+$('firewall-remove').onclick = () => run(async () => { render(await host.call('firewall.remove')); notice(t.ruleRemoved) })
+host.onEvent(event => { if (!event?.state) return; render(event.state); if (event.type === 'paired') notice(t.paired(event.name)) })
+setInterval(tick, 1000)
+// "Last seen" and the live marker change without an event; keep the list fresh while the window is open.
+setInterval(() => { if (!busy && current?.enabled) void refresh() }, 15000)
+void refresh()
diff --git a/apps/desktop/electron/mobile-host.css b/apps/desktop/electron/mobile-host.css
new file mode 100644
index 0000000..58b2959
--- /dev/null
+++ b/apps/desktop/electron/mobile-host.css
@@ -0,0 +1,16 @@
+/* "Telefon bağlantısı" window; builds on apps/mobile/src/style.css (copied to dist/mobile/). */
+.switch { display:flex; align-items:center; gap:10px; margin:22px 0; font-size:1.1rem; cursor:pointer }
+.switch input { width:26px; height:26px; margin:0; flex:none }
+.warning { border-left:3px solid var(--danger); background:var(--surface); padding:10px 14px; margin:16px 0 }
+.warning p { color:var(--text); margin:4px 0 }
+.code { font-family:ui-monospace,Consolas,monospace; font-size:2.6rem; font-weight:750; letter-spacing:.12em; color:var(--text); text-align:center; margin:10px 0; user-select:all }
+.countdown { text-align:center; color:var(--text); font-weight:600 }
+.hint { text-align:center }
+#pair-qr { display:block; margin:14px auto; border-radius:8px; width:260px; height:260px }
+#pair-live p { text-align:center }
+.device { display:flex; align-items:center; justify-content:space-between; gap:12px; border:1px solid var(--stroke); border-radius:8px; padding:10px 14px; margin:10px 0 }
+.device strong { color:var(--text); display:block; overflow-wrap:anywhere }
+.device small { display:block }
+.device button { flex:none; margin:0 }
+footer { margin-top:34px }
+button.link { background:none; color:var(--accent); min-height:0; padding:0; margin:0; font-weight:600; text-decoration:underline }
diff --git a/apps/desktop/electron/mobile-host.html b/apps/desktop/electron/mobile-host.html
index b2ba6ea..5194367 100644
--- a/apps/desktop/electron/mobile-host.html
+++ b/apps/desktop/electron/mobile-host.html
@@ -1,24 +1,27 @@
-
-AITURK Mobil bağlantısı
-
AITURK · BİLGİSAYAR
Telefonunu bağla.
-
Bu pencere açıkken eşleştirdiğiniz Android telefon seçtiğiniz IDE konuşmasına erişebilir. Google anahtarı Windows güvenli kasasında tutulur.
-
Bağlantı kapalı.
-
-
Çıkış, mobil bağlantıları kapatır ve bu penceredeki hesap/Google anahtarını Windows kasasından temizler; başlamış IDE işini durdurmaz.
-
-Gelişmiş: hesap API anahtarı
-
-
-
-
-
-
Telefona izin ver
Telefondaki eşleştirme kimliğini ve 8 karakterli kodu buraya girin. Bu onay yalnız bu telefon ile bu bilgisayarı bağlar.
-
-
-
-
Yalnız kendi telefonunuzun gösterdiği belgenin tamamını buraya aktarın. Onay, bu telefonun sertifikasını bilgisayarınıza kaydeder; farklı sertifika yeniden açık eşleştirme gerektirir.
-
-
Cihazlar
-
Farklı ağlarda bağlantı kurulması ağınıza ve TURN hizmeti yapılandırmasına bağlıdır. Bu geliştirme sürümünde genel ağ erişimi henüz doğrulanmadı.