File size: 22,600 Bytes
9c10fcb
ce0ef24
9c10fcb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17fefbc
9c10fcb
ce0ef24
 
9c10fcb
 
17fefbc
ce0ef24
 
 
 
 
 
 
 
9c10fcb
ce0ef24
 
9c10fcb
 
 
 
ce0ef24
9c10fcb
 
 
 
 
 
 
 
 
 
17fefbc
9c10fcb
ce0ef24
9c10fcb
 
ce0ef24
 
 
 
9c10fcb
ce0ef24
9c10fcb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce0ef24
9c10fcb
 
 
17fefbc
9c10fcb
ce0ef24
9c10fcb
 
 
 
 
ce0ef24
 
9c10fcb
 
 
 
 
ce0ef24
9c10fcb
 
17fefbc
ce0ef24
 
 
 
17fefbc
ce0ef24
 
 
 
 
 
 
9c10fcb
ce0ef24
 
9c10fcb
ce0ef24
9c10fcb
 
17fefbc
ce0ef24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c10fcb
 
 
 
 
 
ce0ef24
9c10fcb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce0ef24
9c10fcb
 
 
 
d86bec8
047a82a
d86bec8
 
9c10fcb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce0ef24
 
9c10fcb
 
 
 
 
 
 
ce0ef24
c92cb2e
 
 
9c10fcb
 
 
 
 
 
 
ce0ef24
9c10fcb
 
 
 
 
 
 
17fefbc
9c10fcb
 
 
 
17fefbc
ce0ef24
 
 
 
9c10fcb
 
 
 
 
 
 
ce0ef24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c10fcb
 
17fefbc
9c10fcb
 
 
 
 
ce0ef24
17fefbc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
<!doctype html><html lang="ar" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>TikTok Live Monitor</title>
<style>
:root{color-scheme:dark;font-family:system-ui,sans-serif}
body{margin:0;background:#0b1220;color:#e6edf7}
main{max-width:1080px;margin:32px auto;padding:0 18px}
h1{font-size:28px;margin:0}h2{font-size:18px;margin:0 0 12px}
p,small{color:#9db0c9;line-height:1.7}
.panel{background:#141c2c;border:1px solid #2a3850;border-radius:16px;padding:20px;margin:18px 0}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px}
.card{border:1px solid #31425c;border-radius:12px;padding:16px}
.card h3{margin:0 0 6px;direction:ltr;text-align:right}
.row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
input,button,select,textarea{font:inherit;border-radius:10px;padding:10px 12px;border:1px solid #3a4d68}
input,select,textarea{background:#0b1220;color:#fff;flex:1;min-width:120px}
button{background:#6fe0b4;color:#10251d;font-weight:650;cursor:pointer}
button.secondary{background:#223249;color:#d5e2f5}
button.danger{background:#3e2430;color:#ffb9c8}
.badge{display:inline-block;border:1px solid #355445;border-radius:999px;padding:3px 10px;color:#78e1b7;font-size:12px}
.badge.off{border-color:#5a3a3a;color:#f0b4b4}
.badge.live{border-color:#3d8;color:#7f6}
.kv{display:grid;grid-template-columns:120px 1fr;gap:6px 10px;font-size:14px}
.kv b{color:#c5d4e8;font-weight:600}
#logs{max-height:260px;overflow:auto;font:13px/1.8 ui-monospace,monospace;background:#0b1220;border-radius:10px;padding:10px}
#error{color:#ffc4c4;white-space:pre-line}
label{display:block;margin:10px 0 6px;color:#c5d4e8}
textarea{width:100%;box-sizing:border-box}
details{margin-top:12px}
footer{color:#8a9bb3;font-size:12px;margin:24px 0}
a{color:#79dcc5}
</style></head><body><main>
<header class="row" style="justify-content:space-between">
  <div><small>TikTok Live Monitor → Direct Relay → YouTube</small><h1>رصد البث المباشر</h1></div>
  <div class="row"><span class="badge" id="sysbadge">System</span><button class="secondary" id="logout" type="button" hidden>خروج</button></div>
</header>
<p id="error" hidden></p>

<section class="panel" id="login-panel">
  <h2>دخول الإدارة</h2>
  <p>استخدم كلمة السر المحفوظة في Secret باسم <code>PASSWORD</code>. لا تستخدم توكن Hugging Face هنا.</p>
  <form id="login" class="row"><input id="admin-password" type="password" autocomplete="current-password" placeholder="كلمة السر" required><button>دخول</button></form>
  <p id="login-error" hidden></p>
</section>

<section class="panel" id="accounts-panel" hidden>
  <h2>Accounts</h2>
  <form id="add" class="row"><input id="username" placeholder="اسم المستخدم أو رابط الحساب" required>
    <input id="token" placeholder="توكن sessionid — اختياري للغرف 18+">
    <button>إضافة ومراقبة</button></form>
  <p class="row"><button class="secondary" id="reload" type="button">تحديث</button><small id="updated"></small></p>
  <div id="tasks" class="cards"></div>
  <details><summary>توكن تيك توك (للبثوث المقيدة عمريًا)</summary>
    <label>الصق كامل سطر Cookie أو JSON المُصدَّر من إضافة الكوكيز</label>
    <textarea id="tokinput" rows="2" dir="ltr" placeholder="sessionid=...; sid_tt=..."></textarea>
    <div class="row" style="margin-top:8px">
      <button id="tokvalidate" class="secondary" type="button">فحص التوكن</button>
      <button id="toksave" type="button">حفظ لكل الحسابات</button>
      <input id="tokuser" dir="ltr" placeholder="اختبار حساب">
      <button id="tokcheck" class="secondary" type="button">اختبر الفتح</button>
    </div>
    <p id="tokresult" hidden></p>
    <div id="tokstate"></div>
  </details>
</section>

<section class="panel" id="youtube-panel" hidden>
  <h2>YouTube</h2>
  <div class="row">
    <button id="ytcheck" class="secondary" type="button">فحص OAuth والقناة</button>
    <button id="ytadopt" class="secondary" type="button" hidden>اعتماد القناة الجديدة فقط</button>
    <button id="ytstart" type="button">تفعيل البث التلقائي</button>
    <button id="ytstop" class="danger" type="button">إيقاف وإنهاء البث الجاري</button>
  </div>
  <p id="ytchannelnotice" role="status" aria-live="polite" hidden></p>
  <div class="kv" style="margin:14px 0">
    <b>الاتصال</b><span id="ytconn">—</span>
    <b>حالة البث</b><span id="ytstate">—</span>
    <b>العنوان الحالي</b><span id="yttitle">—</span>
    <b>الجلسة</b><span id="ytsession">—</span>
    <b>Relay</b><span id="ytrelay">—</span>
  </div>
  <div id="events" class="cards"></div>
  <div class="row">
    <select id="sendmode">
      <option value="auto">auto — نسخ H264 إن ثبت، وإلا تحويل خفيف 720</option>
      <option value="copy">copy — بدون إعادة ترميز</option>
      <option value="fixed720">fixed720 — كانفس ثابت (CPU أعلى)</option>
      <option value="fixed1080">fixed1080 — كانفس 1080</option>
    </select>
    <button class="secondary" id="sendmodeset" type="button">تطبيق الوضع</button>
  </div>
  <details><summary>قوالب العنوان والوصف</summary>
    <form id="settingsform">
      <label>الخصوصية</label>
      <select id="privacy"><option value="public">عام</option><option value="unlisted">غير مُدرج</option><option value="private">خاص</option></select>
      <label>قالب العنوان — الافتراضي: {broadcaster} | {date} {time}</label>
      <input id="titletemplate">
      <label>قالب الوصف</label>
      <textarea id="desctemplate" rows="4"></textarea>
      <p><small>المتغيرات: {broadcaster} {name} {title} {username} {date} {time} {hashtags} {room_id} — التاريخ والوقت بتوقيت بغداد لحظة بدء جلسة تيك توك.</small></p>
      <button type="submit" id="savesettings">حفظ</button>
    </form>
  </details>
</section>

<section class="panel" id="system-panel" hidden>
  <h2>System</h2>
  <div class="kv">
    <b>Monitoring</b><span id="mon">—</span>
    <b>الحسابات</b><span id="nacc">—</span>
    <b>Relay active</b><span id="nrel">—</span>
    <b>CPU</b><span id="ncpu">—</span>
    <b>RAM</b><span id="nram">—</span>
    <b>آخر خطأ</b><span id="lasterr">لا يوجد</span>
  </div>
  <h2 style="margin-top:16px">السجل الحي</h2>
  <div id="logs"></div>
</section>
<footer>لا تسجيل محلي ولا أرشيف فيديو. المسار: TikTok → الذاكرة/الأنبوب → FFmpeg → YouTube RTMPS. أسرار OAuth والتوكنات لا تظهر هنا.</footer>
</main>
<script>
const $=id=>document.getElementById(id),esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
// An OAuth check is the operator's candidate, not the old journal's active channel.
// Periodic refresh must NEVER overwrite it with y.channel from the previous channel.
let settingsLoaded=false,checkedChannel=null,lastY=null,polling=false,pollingSince=0,youtubeBusy=false;
let authHeader='';try{authHeader=sessionStorage.getItem('on-basic-auth')||''}catch{}
function error(text){$('error').textContent=text;$('error').hidden=!text}
function showAuth(ok){
  $('login-panel').hidden=ok;$('accounts-panel').hidden=!ok;$('youtube-panel').hidden=!ok;$('system-panel').hidden=!ok;$('logout').hidden=!ok;
  if(!ok){authHeader='';checkedChannel=null;lastY=null;settingsLoaded=false;renderCheckedChannel();try{sessionStorage.removeItem('on-basic-auth')}catch{};$('admin-password').focus()}
}
function basicHeader(password){
  const bytes=new TextEncoder().encode('admin:'+password);let raw='';for(const b of bytes)raw+=String.fromCharCode(b);return 'Basic '+btoa(raw)
}
async function api(url,method='GET',body){
  const headers={};if(body)headers['Content-Type']='application/json';if(authHeader)headers.Authorization=authHeader;
  const r=await fetch(url,{method,cache:'no-store',headers,body:body?JSON.stringify(body):undefined});
  let j;try{j=await r.json()}catch{}
  if(r.status===401){showAuth(false);throw Error('كلمة السر غير صحيحة أو انتهت جلسة الصفحة')}
  if(!r.ok)throw Error(typeof j?.detail==='string'?j.detail:'تعذر تنفيذ الطلب');return j;
}
function fillSettings(s){$('privacy').value=s.privacy||'public';$('titletemplate').value=s.title||'';$('desctemplate').value=s.description||'';settingsLoaded=true}
function channelSwitchPending(){return !!(checkedChannel?.previous&&checkedChannel.previous.id!==checkedChannel.id)}
function renderCheckedChannel(){
  const pending=channelSwitchPending(),notice=$('ytchannelnotice');
  $('ytadopt').hidden=!pending;notice.hidden=!checkedChannel;
  notice.textContent=!checkedChannel?'':pending
    ? 'القناة الجديدة بعد الفحص: «'+checkedChannel.title+'» ('+checkedChannel.id+') بدل «'+checkedChannel.previous.title+'». لم تُعتمد بعد؛ يمكنك اعتمادها فقط دون تشغيل بث. أحداث القناة السابقة تُؤرشف محليًا ولا تُعدَّل على يوتيوب.'
    : 'القناة المؤكدة بالفحص: «'+checkedChannel.title+'» ('+checkedChannel.id+').';
}
function updateYouTubeControls(){
  const y=lastY||{};
  $('ytcheck').disabled=youtubeBusy;$('ytadopt').disabled=youtubeBusy;
  $('ytstart').disabled=youtubeBusy||!!y.automatic;
  $('ytstop').disabled=youtubeBusy||(!y.automatic&&!(y.events||[]).length);
}
async function checkYouTubeChannel(){
  const j=await api('/api/youtube/check','POST');
  if(!j.channel?.id)throw Error('لم يُرجع الفحص قناة مفوضة');
  checkedChannel=j.channel;renderCheckedChannel();return checkedChannel;
}
function channelConfirmation(channel,start){
  const previous=channel.previous&&channel.previous.id!==channel.id?channel.previous:null;
  let text=previous?'تبديل القناة من «'+previous.title+'» إلى «'+channel.title+'» ('+channel.id+')؟\nستتوقف مرسلات القناة السابقة وتُؤرشف أحداثها محليًا فقط؛ لن تُحذف أو تُعدَّل على يوتيوب.\n'
    :'القناة: «'+channel.title+'» ('+channel.id+').\n';
  return text+(start?'تفعيل Relay التلقائي وإنشاء بث عند رصد LIVE على هذه القناة؟':'اعتماد القناة فقط، مع إبقاء الأتمتة معطلة عند التبديل؟');
}
function stateLabel(st){
  const m={OFFLINE:'Offline',CHECKING:'فحص',LIVE_DETECTED:'اكتشاف LIVE',STARTING:'بدء',LIVE:'LIVE',STALLED:'توقّف المصدر',RECONNECTING:'إعادة اتصال',STOPPING:'إيقاف',ERROR:'خطأ',BACKOFF:'انتظار'};
  return m[st]||st||'—';
}
function fmtDur(s){s=Math.max(0,s|0);const h=Math.floor(s/3600),m=Math.floor((s%3600)/60);return h?h+'س '+m+'د':m+'د '+ (s%60)+'ث'}
async function refresh(){
  if(!authHeader||document.hidden)return;
  if(polling&&Date.now()-(pollingSince||0)<30000)return;
  polling=true;pollingSince=Date.now();
  try{
    const [ry,rs]=await Promise.all([
      api('/api/youtube').then(v=>({v})).catch(e=>({e})),
      api('/api/state').then(v=>({v})).catch(e=>({e}))
    ]);
    const y=ry.v||{events:[],settings:null,channel:null};
    const s0=rs.v||{tasks:[],logs:[]};
    const ytError=ry.e?String(ry.e.message||ry.e):'', stateError=rs.e?String(rs.e.message||rs.e):'';
    if(ry.v)lastY=y;
    const evs=y.events||[], tasks=s0.tasks||[];
    const notes=[]; if(ytError)notes.push(ytError); if(stateError)notes.push(stateError); error(notes.join('\n'));

    $('tasks').innerHTML=tasks.length?tasks.map(x=>{
      const live=x.state==='LIVE';
      return `<article class="card"><h3>@${esc(x.username)}</h3>
        <p><span class="badge ${live?'live':(x.state==='OFFLINE'?'off':'')}">${esc(stateLabel(x.state))}</span>
          ${x.nickname?' · '+esc(x.nickname):''}</p>
        <p>LIVE منذ: ${live?esc(fmtDur(x.live_for||0)):'—'}</p>
        <p>المصدر: ${esc(x.source||'—')}</p>
        <p>Codec: ${esc(x.codec||'—')} · ${esc(x.quality||'')} · room ${esc(x.room_id||'—')}</p>
        <p>PID: ${x.relay_pid||'—'} · بايت: ${esc(x.bytes_received||0)} · إعادة: ${esc(x.restarts||0)}</p>
        <p>آخر بايت: ${x.source_age==null||x.source_age===''?'—':(Number(x.source_age).toFixed(1)+'ث')}</p>
        <p>YouTube: ${esc(x.youtube_status||'—')} · health ${esc(x.youtube_health||'—')}${x.relay_alive?' · Relay':''}</p>
        <small>${esc(x.status||'')}${x.last_error?' · '+esc(x.last_error):''}</small>
        <p>توكن: ${x.has_token?'موجود ✓':'غير مضاف'}</p>
        <div class="row">
          <button data-user="${esc(x.username)}" data-action="start">تشغيل</button>
          <button class="secondary" data-user="${esc(x.username)}" data-action="stop">إيقاف</button>
          <button class="secondary" data-user="${esc(x.username)}" data-action="token">توكن</button>
          <button class="danger" data-user="${esc(x.username)}" data-action="delete">حذف</button>
        </div></article>`;
    }).join(''):'<p>أضف حسابات للمراقبة.</p>';

    const ev=evs[0]||null;
    const conn=[];
    if(y.oauth_configured||y.configured) conn.push(y.channel?('OAuth: '+y.channel.title):'OAuth موجود');
    else conn.push('OAuth غير مفعّل');
    if(y.rtmp_configured||s0.rtmp_configured) conn.push('RTMPS env جاهز');
    else conn.push('لا YOUTUBE_RTMP_URL/KEY');
    if(!(y.metadata_automation||s0.metadata_automation)) conn.push('تحديث العنوان غير متاح بدون OAuth — الفيديو لا يتوقف');
    $('ytconn').textContent=conn.join(' · ');
    $('ytstate').textContent=y.automatic?(y.status||'أتمتة OAuth مفعّلة'):(s0.rtmp_configured?'Relay عبر RTMPS بدون OAuth':'معطّل');
    const liveTask=tasks.find(t=>t.relay_alive||t.state==='LIVE');
    $('yttitle').textContent=ev?(ev.title||'—'):(liveTask&&liveTask.session_title)||'لا بث جارٍ';
    $('ytsession').textContent=ev?(ev.username+' · '+(ev.youtube_status||ev.phase||'')):(liveTask?liveTask.username:'—');
    $('ytrelay').textContent=String(s0.active_relays||evs.length||0)+' نشط';
    $('events').innerHTML=evs.length?evs.map(e=>`<article class="card"><h3>${esc(e.title||'بث')}</h3>
      <p>@${esc(e.username)} · ${esc(e.phase||'')} · YT ${esc(e.youtube_status||'—')}</p>
      <p>المصدر: ${esc(e.source||'—')}</p>
      <p>وضع الإرسال الفعلي: ${esc(e.send_mode||'قيد التحديد')}</p>
      ${e.pending_send_mode?`<p>الترميز الثابت ينتظر خانة: ${esc(e.pending_send_mode)}</p>`:''}
      <small>${esc(e.status||e.note||'')} · ${Math.floor(e.output_seconds||0)} ث</small>
      ${e.broadcast_id?`<p><a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=${encodeURIComponent(e.broadcast_id)}">فتح على يوتيوب</a></p>`:''}
    </article>`).join(''):'<p>عند LIVE يُفتح Relay عبر RTMPS (مفتاح البيئة أو OAuth). فشل تحديث العنوان لا يوقف الفيديو.</p>';

    const lastErr=tasks.map(t=>t.last_error).filter(Boolean)[0] || (s0.logs||[]).filter(l=>/error|فشل|stall|خطأ/i.test(l.message||'')).slice(-1)[0]?.message || 'لا يوجد';
    $('mon').textContent=s0.monitor_running?'يعمل':'متوقف';
    $('nacc').textContent=String(tasks.length);
    $('nrel').textContent=String(s0.active_relays||evs.length||0)+' / '+(Number(s0.max_concurrent_relays)===0?'∞':(s0.max_concurrent_relays||'?'));
    $('ncpu').textContent=(s0.cpu_pct_of_quota!=null
        ? (s0.cpu_pct_of_quota+'% من '+(s0.quota_cpus||'?')+' vCPU')
        : (s0.load1!=null?('load '+s0.load1):'—'));
    $('nram').textContent=(s0.mem_avail_mb!=null?(s0.mem_avail_mb+' MB متاح / '+s0.mem_total_mb+' MB'):'—');
    $('lasterr').textContent=lastErr;
    $('sysbadge').textContent=s0.monitor_running?'Monitor · ON':'Monitor · OFF';
    $('updated').textContent='آخر تحديث '+new Date().toLocaleTimeString('ar-IQ')+' · فحص كل '+(s0.check_interval||'?')+'ث · stall '+(s0.source_stall_timeout||'?')+'ث';

    const cur=y.send_mode_setting||y.send_mode||'auto';
    const sel=$('sendmode'); if(document.activeElement!==sel) sel.value=['auto','fixed720','fixed1080','copy'].includes(cur)?cur:'auto';
    updateYouTubeControls();renderCheckedChannel();
    $('savesettings').disabled=!!y.automatic||!!evs.length;
    if(!settingsLoaded&&y.settings)fillSettings(y.settings);

    $('logs').innerHTML=(s0.logs||[]).slice().reverse().map(l=>`<div>${esc(l.time||'')} · @${esc(l.user||'')} · ${esc(l.message||'')}</div>`).join('')||'<p>لا أحداث بعد.</p>';
    renderTiktok(s0.tiktok);
  } finally {polling=false}
}
function renderTiktok(t){if(!t){$('tokstate').innerHTML='';return}
  const accts=Object.entries(t.accounts||{});
  let rows=`<small>توكن عام: ${t.default_authenticated?esc(t.default_masked):'غير مضبوط'}</small>`;
  rows+=accts.map(([u,v])=>`<div>@${esc(u)} — ${v.authenticated?esc(v.masked):'يعتمد على العام'}</div>`).join('');
  $('tokstate').innerHTML=rows}
function tokShow(html){const e=$('tokresult');e.innerHTML=html;e.hidden=false}
$('login').onsubmit=async e=>{e.preventDefault();const p=$('admin-password').value;const msg=$('login-error');msg.hidden=true;
  authHeader=basicHeader(p);try{await api('/api/state');try{sessionStorage.setItem('on-basic-auth',authHeader)}catch{};$('admin-password').value='';showAuth(true);await refresh()}
  catch(err){showAuth(false);msg.textContent='تعذر الدخول: '+String(err.message||err);msg.hidden=false}};
$('logout').onclick=()=>{showAuth(false);error('');$('login-error').hidden=true};
$('tokvalidate').onclick=async()=>{const raw=$('tokinput').value.trim();if(!raw){tokShow('الصق التوكن أولًا');return}
  try{$('tokvalidate').disabled=true;const r=await api('/api/tiktok/token/validate','POST',{token:raw});tokShow(esc(r.message||''));if(r.ok)await refresh()}catch(e){tokShow(esc(e.message))}finally{$('tokvalidate').disabled=false}};
$('toksave').onclick=async()=>{const raw=$('tokinput').value.trim();if(!raw){tokShow('الصق التوكن أولًا');return}
  try{$('toksave').disabled=true;const r=await api('/api/tiktok/token/validate','POST',{token:raw});tokShow(esc(r.message||''));if(r.ok){$('tokinput').value='';await refresh()}}catch(e){tokShow(esc(e.message))}finally{$('toksave').disabled=false}};
$('tokcheck').onclick=async()=>{const u=$('tokuser').value.trim();if(!u){tokShow('اكتب اسم الحساب');return}
  try{$('tokcheck').disabled=true;const r=await api('/api/tiktok/token/check/'+encodeURIComponent(u),'POST');tokShow(esc(r.message||r.result||''))}catch(e){tokShow(esc(e.message))}finally{$('tokcheck').disabled=false}};
$('sendmodeset').onclick=async()=>{try{$('sendmodeset').disabled=true;await api('/api/youtube/settings','POST',{send_mode:$('sendmode').value});await refresh()}catch(e){error(e.message)}finally{$('sendmodeset').disabled=false}};
$('ytcheck').onclick=async()=>{
  if(youtubeBusy)return;youtubeBusy=true;updateYouTubeControls();
  try{const channel=await checkYouTubeChannel();await refresh();alert('القناة المفوضة: '+channel.title+(channelSwitchPending()?' — بانتظار تأكيد اعتمادها':''))}
  catch(e){error(e.message)}finally{youtubeBusy=false;updateYouTubeControls()}
};
$('ytadopt').onclick=async()=>{
  if(youtubeBusy)return;youtubeBusy=true;updateYouTubeControls();
  try{
    const channel=await checkYouTubeChannel();
    if(!confirm(channelConfirmation(channel,false)))return;
    const j=await api('/api/youtube/channel','POST',{channel_id:channel.id});
    checkedChannel=j.channel;renderCheckedChannel();await refresh();
    alert('تم اعتماد القناة «'+j.channel.title+'». اعتماد القناة وحده لا يبدأ أي بث.');
  }catch(e){error(e.message)}finally{youtubeBusy=false;updateYouTubeControls()}
};
$('ytstart').onclick=async()=>{
  if(youtubeBusy)return;youtubeBusy=true;updateYouTubeControls();
  try{
    // Recheck even after a previous check: OAuth Secrets may have changed in another tab.
    const channel=await checkYouTubeChannel();
    if(!confirm(channelConfirmation(channel,true)))return;
    await api('/api/youtube/start','POST',{channel_id:channel.id});
    checkedChannel={id:channel.id,title:channel.title};renderCheckedChannel();await refresh();
  }catch(e){error(e.message)}finally{youtubeBusy=false;updateYouTubeControls()}
};
$('ytstop').onclick=async()=>{
  if(youtubeBusy||!confirm('إيقاف الأتمتة وإنهاء البث الجاري؟'))return;
  youtubeBusy=true;updateYouTubeControls();
  try{await api('/api/youtube/stop','POST');await refresh()}catch(e){error(e.message)}finally{youtubeBusy=false;updateYouTubeControls()}
};
$('settingsform').onsubmit=async e=>{e.preventDefault();try{await api('/api/youtube/settings','POST',{name:'',privacy:$('privacy').value,title:$('titletemplate').value,description:$('desctemplate').value});settingsLoaded=false;await refresh()}catch(e){error(e.message)}};
$('add').onsubmit=async e=>{e.preventDefault();try{await api('/api/accounts','POST',{username:$('username').value,token:$('token').value});$('username').value='';$('token').value='';await refresh()}catch(e){error(e.message)}};
document.addEventListener('click',async e=>{const b=e.target.closest('button');if(!b)return;try{
  if(b.dataset.action==='token'){const t=prompt('توكن جلسة تيك توك للحساب @'+b.dataset.user+' (فارغ للحذف):');if(t===null)return;await api('/api/accounts/'+encodeURIComponent(b.dataset.user)+'/token','POST',{token:t});await refresh();return}
  if(b.dataset.action){if(b.dataset.action==='delete'&&!confirm('حذف الحساب؟'))return;await api('/api/accounts/'+encodeURIComponent(b.dataset.user)+'/'+b.dataset.action,'POST');await refresh()}
}catch(e){error(e.message)}});
$('reload').onclick=()=>refresh();
document.addEventListener('visibilitychange',()=>{if(!document.hidden)refresh()});
showAuth(!!authHeader);if(authHeader)refresh();setInterval(refresh,10000);
</script></body></html>