Uzun sure acik kalan IDE'de "Telefon ekle" ilk istegi olu bir keep-alive baglantisinda kaybedebiliyordu; bulusma kaydi yayinlanamayinca kod yolu bes dakika boyunca "Kodla baglanma su an kullanilamiyor" kaliyordu (QR calisiyordu). - publishPairOffer: en cok 4 deneme (0,5 / 1,5 / 3 sn); ag hatasi, 5xx ve 429 yeniden denenir, 422 kalicidir; yaniti kaybolan denemeden sonraki 409 kaydin yerinde oldugu anlamina gelir (iptal jetonu yoktur, kayit kendiliginden biter). - Pencerede "Yeniden dene": ayni kod ve QR ile kaydi yeniden yayinlar. - Role oturumu kaydi ve "Giris yap" baslangici icin tek ek deneme (zaman asimi yeniden denenmez). - Gunluge yalniz "pair offer published / failed after N tries" duser; kod, lookup ve jeton yazilmaz. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8ppceaR5s6cvMnm7uGRnf
170 lines
14 KiB
JavaScript
170 lines
14 KiB
JavaScript
// "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: 'Telefonunuz internetin olduğu her yerden bu bilgisayardaki IDE’ye bağlanır; aynı Wi-Fi gerekmez. Bağlantı uçtan uca şifrelidir. Bilgisayar açık ve AITURK IDE çalışıyor olmalı.',
|
||
toggle: 'Telefon bağlantısını kullan',
|
||
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.',
|
||
relayLabel: 'İnternet üzerinden bağlantı', lanLabel: 'Aynı Wi-Fi’da doğrudan bağlantı',
|
||
relay: { connected: 'hazır', connecting: 'bağlanıyor…', failed: 'kurulamadı (yeniden denenecek)', idle: 'beklemede (telefon eklenince bağlanır)',
|
||
replaced: 'başka bir AITURK IDE aynı kaydı kullanıyor (30 sn sonra yeniden denenecek)', busy: 'servis dolu (yeniden denenecek)' },
|
||
lanOpen: port => `açık (bağlantı noktası ${port})`, lanOff: 'kapalı', lanIdle: 'izin verildi (telefon eklenince açılır)',
|
||
lanNoRule: 'Windows Güvenlik Duvarı izni yok', lanDenied: 'kapalı (Windows izin penceresi reddedildi)', lanFailed: 'kapalı (Güvenlik Duvarı kuralı eklenemedi)',
|
||
lanAllow: 'Aynı Wi-Fi’da doğrudan bağlantıya da izin ver', allow: 'İzin ver', lanDisallow: 'Doğrudan bağlantıyı kapat',
|
||
reissued: 'Röle kimliği yenilendi. Daha önce eklenen telefonları yeniden eşleştirin: telefonda bu bilgisayarı kaldırıp “Telefon ekle” ile yeniden bağlayın.',
|
||
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.', codeRetry: 'Yeniden dene',
|
||
securityCode: code => `Güvenlik kodu: ${code} (telefonda da aynı kod görünmeli)`,
|
||
anyNetwork: 'Telefon ve bilgisayar aynı ağda olmak zorunda değil.', 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ı.`,
|
||
removeRule: 'Güvenlik Duvarı kuralını kaldır', ruleRemoved: 'Güvenlik Duvarı kuralı kaldırıldı.',
|
||
renew: 'Röle kimliğini yenile', renewed: 'Röle kimliği yenilendi. Telefonları yeniden eşleştirin.',
|
||
renewConfirm: 'Röle kimliği yenilenirse bağlı telefonların hepsi kaldırılır ve yeniden eşleştirilmeleri gerekir. Devam edilsin mi?',
|
||
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 ve internet üzerinden bağlantı da kurulamadı.',
|
||
relay_unavailable: 'İnternete ulaşılamıyor, yeniden deneyin.',
|
||
port_unavailable: 'Boş bağlantı noktası bulunamadı (47321–47340 kullanımda).',
|
||
failed: 'İşlem tamamlanamadı. Yeniden deneyin.',
|
||
},
|
||
},
|
||
en: {
|
||
eyebrow: 'AITURK · COMPUTER', title: 'Phone connection',
|
||
intro: 'Your phone connects to the IDE on this computer from anywhere with internet access; the same Wi-Fi is not needed. The connection is end-to-end encrypted. The computer must be on and AITURK IDE must be running.',
|
||
toggle: 'Use phone connection',
|
||
remote: 'The IDE is connected to a remote gateway. Phone connection only works with the local IDE connection; choose the local connection in Settings.',
|
||
relayLabel: 'Connection over the internet', lanLabel: 'Direct connection on the same Wi-Fi',
|
||
relay: { connected: 'ready', connecting: 'connecting…', failed: 'could not be set up (will retry)', idle: 'waiting (connects once a phone is added)',
|
||
replaced: 'another AITURK IDE is using the same registration (will retry in 30 s)', busy: 'service is full (will retry)' },
|
||
lanOpen: port => `on (port ${port})`, lanOff: 'off', lanIdle: 'allowed (opens once a phone is added)',
|
||
lanNoRule: 'no Windows Firewall permission', lanDenied: 'off (the Windows permission prompt was declined)', lanFailed: 'off (the Firewall rule could not be added)',
|
||
lanAllow: 'Also allow a direct connection on the same Wi-Fi', allow: 'Allow', lanDisallow: 'Turn direct connection off',
|
||
reissued: 'The relay identity was renewed. Pair the phones added earlier again: remove this computer on the phone and connect with “Add a phone”.',
|
||
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.', codeRetry: 'Try again',
|
||
securityCode: code => `Security code: ${code} (the phone must show the same code)`,
|
||
anyNetwork: 'The phone and the computer do not have to be on the same network.', 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.`,
|
||
removeRule: 'Remove the Firewall rule', ruleRemoved: 'The Firewall rule was removed.',
|
||
renew: 'Renew the relay identity', renewed: 'The relay identity was renewed. Pair the phones again.',
|
||
renewConfirm: 'Renewing the relay identity removes every connected phone; they must be paired again. Continue?',
|
||
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) and the internet connection could not be set up either.',
|
||
relay_unavailable: 'The internet cannot be reached, try again.',
|
||
port_unavailable: 'No free port was found (47321–47340 are in use).',
|
||
failed: 'That did not work. Try again.',
|
||
},
|
||
},
|
||
}
|
||
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-devices', 'devices'], ['devices-empty', 'noDevices'], ['t-enabled', 'toggle'], ['t-relay-label', 'relayLabel'],
|
||
['t-lan-label', 'lanLabel'], ['t-reissued', 'reissued'], ['lan-disallow', 'lanDisallow'], ['relay-renew', 'renew'],
|
||
['pair-begin', 'addPhone'], ['pair-cancel', 'cancel'], ['pair-republish', 'codeRetry'], ['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() }
|
||
}
|
||
|
||
function render(state) {
|
||
current = state
|
||
$('enabled').checked = state.enabled
|
||
$('remote-box').hidden = !state.remote
|
||
// Status rows say what is REALLY up right now: the relay socket, the LAN socket, the Windows rule as queried.
|
||
const relay = state.relay || { status: 'off' }, lan = state.lan || { direct: false, listening: false, port: null, firewall: 'unknown' }
|
||
$('status-box').hidden = !state.enabled || state.remote
|
||
const relayKind = relay.status === 'failed' && t.relay[relay.code] ? relay.code : relay.status === 'off' ? 'idle' : relay.status
|
||
$('relay-status').textContent = t.relay[relayKind]; $('relay-status').dataset.state = relay.status
|
||
$('reissued-box').hidden = !(state.enabled && relay.reissued)
|
||
const noRule = lan.direct && lan.firewall === 'missing'
|
||
$('lan-status').textContent = lan.listening ? t.lanOpen(lan.port) : noRule ? t.lanNoRule : lan.direct ? t.lanIdle
|
||
: state.firewall === 'denied' ? t.lanDenied : state.firewall === 'failed' ? t.lanFailed : t.lanOff
|
||
$('lan-status').dataset.state = lan.listening ? 'connected' : noRule ? 'failed' : 'off'
|
||
$('lan-allow').hidden = lan.direct && !noRule; $('lan-allow').textContent = noRule ? t.allow : t.lanAllow
|
||
$('lan-disallow').hidden = !lan.direct
|
||
$('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]
|
||
// Offered only once every background try is over; while they run the line above says "hazırlanıyor…".
|
||
$('pair-republish').hidden = state.pairing.codePath !== 'unavailable'
|
||
$('pair-security').textContent = state.pairing.securityCode ? t.securityCode(state.pairing.securityCode) : ''
|
||
$('t-same-wifi').textContent = /[?&]r=/.test(state.pairing.qr) ? t.anyNetwork : t.sameWifi
|
||
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.enabled || state.remote
|
||
$('relay-renew').hidden = !relay.ready
|
||
$('firewall-remove').hidden = lan.firewall !== 'allowed' || state.firewall === 'not-needed'
|
||
}
|
||
|
||
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')) })
|
||
$('pair-republish').onclick = () => run(async () => { render(await host.call('pair.republish')) })
|
||
$('lan-allow').onclick = () => run(async () => { notice(''); render(await host.call('lan.allow')) })
|
||
$('lan-disallow').onclick = () => run(async () => { notice(''); render(await host.call('lan.disallow')) })
|
||
$('relay-renew').onclick = () => { if (window.confirm(t.renewConfirm)) void run(async () => { render(await host.call('relay.renew')); notice(t.renewed) }) }
|
||
$('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()
|