iobroker.jetframe 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +357 -0
  3. package/admin/SF-Pro.ttf +0 -0
  4. package/admin/admin.d.ts +65 -0
  5. package/admin/frame.html +982 -0
  6. package/admin/frame.html.bak-aircraft-card-real-row-20260518-1608 +1236 -0
  7. package/admin/frame.html.bak-aircraft-card-structure-20260518-1517 +1236 -0
  8. package/admin/frame.html.bak-aircraft-logo-id-fix-20260518-1639 +1239 -0
  9. package/admin/frame.html.bak-shortcut-test +1236 -0
  10. package/admin/frame.html.bak-tablet-class-20260518-1729 +1239 -0
  11. package/admin/heatmap.html +216 -0
  12. package/admin/index.html +268 -0
  13. package/admin/index_m.html +1749 -0
  14. package/admin/jetframe.css +1260 -0
  15. package/admin/jetframe.css.bak-airbus-landscape-fix +4630 -0
  16. package/admin/jetframe.css.bak-aircraft-card-clean-equal-20260518-1438 +4899 -0
  17. package/admin/jetframe.css.bak-aircraft-card-real-row-20260518-1608 +4814 -0
  18. package/admin/jetframe.css.bak-aircraft-card-row-left-20260518-1525 +4604 -0
  19. package/admin/jetframe.css.bak-aircraft-card-slim-equal-20260518-1446 +4647 -0
  20. package/admin/jetframe.css.bak-aircraft-card-structure-20260518-1517 +4646 -0
  21. package/admin/jetframe.css.bak-aircraft-inline-final-20260518-1527 +4654 -0
  22. package/admin/jetframe.css.bak-aircraft-row-compact-fix-20260518-1639 +4763 -0
  23. package/admin/jetframe.css.bak-before-aircrafttype-purge +4818 -0
  24. package/admin/jetframe.css.bak-before-cleanup +4670 -0
  25. package/admin/jetframe.css.bak-before-remove-tablet-only-20260518-1711 +4896 -0
  26. package/admin/jetframe.css.bak-before-tablet-layout-rework-20260518-1650 +4914 -0
  27. package/admin/jetframe.css.bak-clean-duplicate-fonts-20260518-1340 +4975 -0
  28. package/admin/jetframe.css.bak-clean-old-index-fix-20260518-1937 +5167 -0
  29. package/admin/jetframe.css.bak-hardleft-airbus +4751 -0
  30. package/admin/jetframe.css.bak-index-iphone-landscape-20260518-1931 +5030 -0
  31. package/admin/jetframe.css.bak-index-landscape-final-20260518-1941 +5167 -0
  32. package/admin/jetframe.css.bak-index-landscape-real-20260518-1936 +5186 -0
  33. package/admin/jetframe.css.bak-landscape-compact-jumbo-bold-20260518-1343 +4802 -0
  34. package/admin/jetframe.css.bak-logo-align-final +4551 -0
  35. package/admin/jetframe.css.bak-logo-final2 +4551 -0
  36. package/admin/jetframe.css.bak-narrowbody-font-fix +4992 -0
  37. package/admin/jetframe.css.bak-nuke-airbus-align +4790 -0
  38. package/admin/jetframe.css.bak-pill-balance-20260518-1603 +4773 -0
  39. package/admin/jetframe.css.bak-pill-balance-fix +4910 -0
  40. package/admin/jetframe.css.bak-radar-fix-fonts +4710 -0
  41. package/admin/jetframe.css.bak-shortcut-test +4899 -0
  42. package/admin/jetframe.css.bak-smaller-aircraft-card-fonts-20260518-1345 +4897 -0
  43. package/admin/jetframe.css.bak-tablet-fix-real-20260518-1748 +4945 -0
  44. package/admin/jetframe.css.bak-tablet-fullscreen-fix-20260518-1804 +4972 -0
  45. package/admin/jetframe.css.bak-tablet-landscape-layout-20260518-1645 +4802 -0
  46. package/admin/jetframe.css.bak-tablet-layout-final-20260518-1839 +4802 -0
  47. package/admin/jetframe.css.bak-tablet-layout-v3-20260518-1729 +4802 -0
  48. package/admin/jetframe.css.bak-tablet-layout-v4-20260518-1801 +4957 -0
  49. package/admin/jetframe.css.bak-tablet-layout-v5-20260518-1843 +4970 -0
  50. package/admin/jetframe.css.bak-tablet-layout-v6-20260518-1848 +4958 -0
  51. package/admin/jetframe.css.bak-tablet-layout-v7-20260518-1909 +4985 -0
  52. package/admin/jetframe.css.bak-tablet-only-landscape-v2-20260518-1707 +4802 -0
  53. package/admin/jetframe.css.bak-tablet-pages-final-20260519-1857 +5188 -0
  54. package/admin/jetframe.css.bak-tablet-pages-final-20260519-1859 +5347 -0
  55. package/admin/jetframe.css.bak-tablet-pages-v2-20260519-190807 +5349 -0
  56. package/admin/jetframe.css.bak-typography-align-final +4818 -0
  57. package/admin/jetframe.png +0 -0
  58. package/admin/manifest.webmanifest +15 -0
  59. package/admin/src/app.tsx +58 -0
  60. package/admin/src/components/settings.tsx +97 -0
  61. package/admin/src/i18n/de.json +11 -0
  62. package/admin/src/i18n/en.json +11 -0
  63. package/admin/src/i18n/es.json +11 -0
  64. package/admin/src/i18n/fr.json +11 -0
  65. package/admin/src/i18n/i18n.d.ts +28 -0
  66. package/admin/src/i18n/it.json +11 -0
  67. package/admin/src/i18n/nl.json +11 -0
  68. package/admin/src/i18n/pl.json +11 -0
  69. package/admin/src/i18n/pt.json +11 -0
  70. package/admin/src/i18n/ru.json +11 -0
  71. package/admin/src/i18n/uk.json +11 -0
  72. package/admin/src/i18n/zh-cn.json +11 -0
  73. package/admin/src/index.tsx +25 -0
  74. package/admin/stats.html +228 -0
  75. package/admin/style.css +32 -0
  76. package/admin/tsconfig.json +11 -0
  77. package/admin/words.js +46 -0
  78. package/build/lib/adsb.js +218 -0
  79. package/build/lib/adsb.js.map +7 -0
  80. package/build/lib/airportNamesDe.js +131 -0
  81. package/build/lib/airportNamesDe.js.map +7 -0
  82. package/build/lib/airports.js +281 -0
  83. package/build/lib/airports.js.map +7 -0
  84. package/build/lib/classify.js +339 -0
  85. package/build/lib/classify.js.map +7 -0
  86. package/build/lib/config.js +103 -0
  87. package/build/lib/config.js.map +7 -0
  88. package/build/lib/flightInfo.js +1409 -0
  89. package/build/lib/flightInfo.js.map +7 -0
  90. package/build/lib/geo.js +84 -0
  91. package/build/lib/geo.js.map +7 -0
  92. package/build/lib/images.js +422 -0
  93. package/build/lib/images.js.map +7 -0
  94. package/build/lib/specialLiveries.js +342 -0
  95. package/build/lib/specialLiveries.js.map +7 -0
  96. package/build/lib/states.js +971 -0
  97. package/build/lib/states.js.map +7 -0
  98. package/build/lib/staticFiles.js +73 -0
  99. package/build/lib/staticFiles.js.map +7 -0
  100. package/build/lib/types.js +17 -0
  101. package/build/lib/types.js.map +7 -0
  102. package/build/lib/visConfig.js +52 -0
  103. package/build/lib/visConfig.js.map +7 -0
  104. package/build/main.js +1454 -0
  105. package/build/main.js.map +7 -0
  106. package/io-package.json +169 -0
  107. package/package.json +82 -0
@@ -0,0 +1,216 @@
1
+ <!doctype html>
2
+ <html lang="de">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"/>
6
+ <title>JetFrame Heatmap</title>
7
+ <link rel="manifest" href="manifest.webmanifest">
8
+ <link rel="apple-touch-icon" href="jetframe.png">
9
+ <link rel="icon" type="image/png" href="jetframe.png">
10
+ <meta name="apple-mobile-web-app-capable" content="yes">
11
+ <meta name="mobile-web-app-capable" content="yes">
12
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
13
+ <meta name="theme-color" content="#050709">
14
+ <link rel="stylesheet" href="jetframe.css?v=20260522-fix14">
15
+ </head>
16
+ <body class="jf-page-heatmap">
17
+
18
+ <div id="simpleApiWarning" class="simpleApiWarning">
19
+ ⚠️ Simple-API nicht erreichbar<br>
20
+ <span id="simpleApiWarningText"></span>
21
+ </div>
22
+
23
+ <div id="app">
24
+ <div class="jf-shell">
25
+ <div class="header">
26
+ <a class="backBtn" href="index.html" aria-label="Zurück">‹</a>
27
+ <div>
28
+ <div class="title">✈️ JetFrame</div>
29
+ <div id="dateText" class="sub">Heatmap</div>
30
+ </div>
31
+ <div class="livePill">
32
+ <span class="dot"></span><span>Live</span>
33
+ </div>
34
+ </div>
35
+
36
+ <div class="hmStats">
37
+ <div class="hmStat"><div id="totalFlights" class="hmVal">–</div><div class="hmLabel">Flüge</div></div>
38
+ <div class="hmStat"><div id="landings" class="hmVal">–</div><div class="hmLabel">Landungen</div></div>
39
+ <div class="hmStat"><div id="departures" class="hmVal">–</div><div class="hmLabel">Starts</div></div>
40
+ <div class="hmStat"><div id="overflights" class="hmVal">–</div><div class="hmLabel">Überflüge</div></div>
41
+ </div>
42
+
43
+ <div id="rushBox" class="rush">
44
+ <span id="rushText">Lade Traffic…</span>
45
+ <span id="bestText">Beste Zeit: –</span>
46
+ </div>
47
+
48
+ <div id="hourScroller" class="scroller"></div>
49
+
50
+ <div class="rankings">
51
+ <div class="panel"><h3>Top Airlines</h3><pre id="airlineRanking">–</pre></div>
52
+ <div class="panel"><h3>Top Routen</h3><pre id="routeRanking">–</pre></div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <script>
58
+ 'use strict';
59
+ let API_BASE = (() => {
60
+ const u = new URL(location.href);
61
+ return (location.protocol||'http:')+'//'+(u.searchParams.get('apiHost')||location.hostname||'127.0.0.1')+':'+(u.searchParams.get('apiPort')||'8087');
62
+ })();
63
+ const DP_ROOT = 'jetframe.'+(new URL(location.href).searchParams.get('instance')||'0').replace(/[^0-9]/g,'');
64
+ let VIS_CONFIG_LOADED = false;
65
+
66
+ const BASE = DP_ROOT+'.statistics.today';
67
+ const IDS = {
68
+ date:'date', totalFlights:'totalFlights', landings:'landings',
69
+ departures:'departures', overflights:'overflights', hourly:'hourly',
70
+ bestSpotterHour:'bestSpotterHour', rushHourNow:'rushHourNow',
71
+ rushHourText:'rushHourText', airlineRankingText:'airlineRankingText',
72
+ routeRankingText:'routeRankingText',
73
+ };
74
+
75
+ function $(id){ return document.getElementById(id); }
76
+
77
+ function setWarn(v,t){ const b=$('simpleApiWarning'),m=$('simpleApiWarningText'); if(!b)return; if(m)m.textContent=t||API_BASE; b.classList.toggle('visible',!!v); }
78
+
79
+ async function checkApi(){ try{ const r=await fetch(API_BASE+'/getPlainValue/'+encodeURIComponent(DP_ROOT+'.status'),{cache:'no-store'}); if(!r.ok)throw 0; setWarn(false); return true; }catch{ setWarn(true); return false; } }
80
+
81
+ async function loadVis(){
82
+ const u=new URL(location.href);
83
+ const noOverride=!u.searchParams.has('apiHost')&&!u.searchParams.has('apiPort');
84
+ const r=await fetch('/jetframe.admin/vis-config.json?v='+Date.now(),{cache:'no-store'});
85
+ if(!r.ok)throw new Error('vis-config.json fehlt');
86
+ const c=await r.json();
87
+ if(noOverride){ API_BASE=(location.protocol||'http:')+'//'+String(c.simpleApiHost||location.hostname||'127.0.0.1').trim()+':'+String(c.simpleApiPort||'8087').trim(); }
88
+ VIS_CONFIG_LOADED=true;
89
+ }
90
+
91
+ function clean(v){
92
+ if(v==null)return''; v=String(v).trim();
93
+ if(v==='null'||v==='undefined')return'';
94
+ if((v[0]==='"'&&v.slice(-1)==='"')||(v[0]==="'"&&v.slice(-1)==="'"))v=v.slice(1,-1);
95
+ return v.replace(/\\"/g,'"').replace(/\\n/g,'\n').trim();
96
+ }
97
+
98
+ function num(v){ const n=Number(String(v).replace(',','.')); return Number.isFinite(n)?n:0; }
99
+
100
+ async function read(key){
101
+ if(!VIS_CONFIG_LOADED)return'';
102
+ try{
103
+ const r=await fetch(API_BASE+'/getPlainValue/'+encodeURIComponent(BASE+'.'+key),{cache:'no-store'});
104
+ if(!r.ok)return'';
105
+ return clean(await r.text());
106
+ }catch{ setWarn(true); return''; }
107
+ }
108
+
109
+ async function readAll(){ return Object.fromEntries(await Promise.all(Object.keys(IDS).map(async k=>[k,await read(IDS[k])]))); }
110
+
111
+ function parseHourly(raw){
112
+ const r={};
113
+ for(let h=0;h<24;h++)r[String(h).padStart(2,'0')]={total:0,landings:0,departures:0,overflights:0};
114
+ try{ const d=JSON.parse(raw||'{}'); for(const[k,v]of Object.entries(d))if(r[k])r[k]={total:+v.total||0,landings:+v.landings||0,departures:+v.departures||0,overflights:+v.overflights||0}; }catch{}
115
+ return r;
116
+ }
117
+
118
+ function renderHours(hourly){
119
+ const sc=$('hourScroller'); if(!sc)return;
120
+ const nowH=String(new Date().getHours()).padStart(2,'0');
121
+ const max=Math.max(1,...Object.values(hourly).map(v=>v.total));
122
+ const best=Object.entries(hourly).sort((a,b)=>b[1].total-a[1].total)[0]?.[0]||'';
123
+ const hash=JSON.stringify(hourly)+nowH;
124
+ if(sc.dataset.hash===hash)return;
125
+ sc.dataset.hash=hash;
126
+ sc.innerHTML='';
127
+ const f=document.createDocumentFragment();
128
+ for(let h=0;h<24;h++){
129
+ const k=String(h).padStart(2,'0'), v=hourly[k]||{total:0,landings:0,departures:0,overflights:0};
130
+ const heat=Math.min(.72,Math.max(.06,v.total/max*.72));
131
+ const c=document.createElement('div');
132
+ c.className='hourCard'+(k===nowH?' now':'')+(k===best&&v.total>0?' best':'');
133
+ c.style.setProperty('--heat',heat.toFixed(2));
134
+ const badge=k===nowH?'JETZT':(k===best&&v.total>0?'PEAK':'UHR');
135
+ c.innerHTML='<div class="hourInner"><div class="hourTop"><div class="hour">'+k+':00</div><div class="badge">'+badge+'</div></div><div class="hourTotal">'+v.total+'</div><div class="hourSub">Flugbewegungen</div><div class="bars"><div class="bar">🛬 '+v.landings+'</div><div class="bar">🛫 '+v.departures+'</div><div class="bar">🛩️ '+v.overflights+'</div></div></div>';
136
+ f.appendChild(c);
137
+ }
138
+ sc.appendChild(f);
139
+ setTimeout(()=>{ const n=sc.querySelector('.now'); if(n)n.scrollIntoView({behavior:'smooth',inline:'center',block:'nearest'}); },150);
140
+ }
141
+
142
+ function set(id,val){ const e=$(id); if(e)e.textContent=val; }
143
+
144
+ function render(d){
145
+ set('dateText', d.date?'Heute · '+d.date:'Heatmap');
146
+ set('totalFlights', num(d.totalFlights));
147
+ set('landings', num(d.landings));
148
+ set('departures', num(d.departures));
149
+ set('overflights', num(d.overflights));
150
+ const rush=$('rushBox'); if(rush)rush.classList.toggle('hot',clean(d.rushHourNow)==='true');
151
+ set('rushText', clean(d.rushHourText)||'Noch keine Rushhour');
152
+ set('bestText', clean(d.bestSpotterHour)?'Beste Zeit: '+clean(d.bestSpotterHour):'Beste Zeit: –');
153
+ set('airlineRanking', clean(d.airlineRankingText)||'–');
154
+ set('routeRanking', clean(d.routeRankingText)||'–');
155
+ renderHours(parseHourly(d.hourly));
156
+ }
157
+
158
+ async function poll(){ await checkApi(); render(await readAll()); }
159
+
160
+ loadVis().then(async()=>{ await checkApi(); poll(); setInterval(poll,10000); })
161
+ .catch(e=>{ VIS_CONFIG_LOADED=false; console.error(e); setWarn(true,'vis-config.json fehlt'); });
162
+ </script>
163
+
164
+ <script>
165
+ /* Header clock — every 5s */
166
+ (function(){
167
+ let _b=false;
168
+ function pad(n){return String(n).padStart(2,'0');}
169
+ function wanted(){const d=new Date();return'Heute · '+pad(d.getHours())+':'+pad(d.getMinutes())+' · '+pad(d.getDate())+'.'+pad(d.getMonth()+1)+'.'+d.getFullYear();}
170
+ function apply(){if(_b)return;_b=true;const s=document.querySelector('.header .sub');if(s){const t=wanted();if(s.textContent!==t)s.textContent=t;}_b=false;}
171
+ apply();setInterval(apply,5000);
172
+ const h=document.querySelector('.header');if(h)new MutationObserver(apply).observe(h,{childList:true,subtree:true,characterData:true});
173
+ })();
174
+ </script>
175
+
176
+ <script>
177
+ /* Date format YYYY-MM-DD → DD.MM.YYYY */
178
+ (function(){
179
+ const RE=/\b(\d{4})-(\d{2})-(\d{2})\b/g;
180
+ function fmt(t){return String(t||'').replace(RE,'$3.$2.$1');}
181
+ function apply(){const root=document.querySelector('.jf-shell');if(!root)return;const w=document.createTreeWalker(root,NodeFilter.SHOW_TEXT);const ns=[];while(w.nextNode())ns.push(w.currentNode);for(const n of ns){if(!RE.test(n.nodeValue))continue;const nx=fmt(n.nodeValue);if(nx!==n.nodeValue)n.nodeValue=nx;}}
182
+ apply();setInterval(apply,5000);
183
+ const r=document.querySelector('.jf-shell');if(r)new MutationObserver(apply).observe(r,{childList:true,subtree:true,characterData:true});
184
+ })();
185
+ </script>
186
+
187
+ <script>
188
+ /* Ranking-Zeilen: jede Zeile als eigener Span mit ellipsis */
189
+ (function(){
190
+ function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
191
+ function applyOne(pre){
192
+ if(!pre)return;
193
+ // Originaltext merken
194
+ if(pre.dataset.jfRaw===undefined) pre.dataset.jfRaw=pre.textContent||'';
195
+ else if(!pre.querySelector('.hm-line')) pre.dataset.jfRaw=pre.textContent||'';
196
+ const raw=(pre.dataset.jfRaw||'').trim();
197
+ if(!raw||raw==='–'||raw==='-'){pre.classList.remove('hm-list');pre.textContent=raw||'–';return;}
198
+ const lines=raw.split(/\n+/).map(s=>s.trim()).filter(Boolean);
199
+ if(lines.length<1)return;
200
+ pre.classList.add('hm-list');
201
+ const html=lines.map(l=>'<span class="hm-line">'+esc(l)+'</span>').join('');
202
+ if(pre.innerHTML!==html)pre.innerHTML=html;
203
+ }
204
+ function applyAll(){
205
+ applyOne(document.getElementById('airlineRanking'));
206
+ applyOne(document.getElementById('routeRanking'));
207
+ }
208
+ let t=null;
209
+ new MutationObserver(()=>{clearTimeout(t);t=setTimeout(applyAll,30);})
210
+ .observe(document.body,{childList:true,subtree:true,characterData:true});
211
+ applyAll();
212
+ })();
213
+ </script>
214
+
215
+ </body>
216
+ </html>
@@ -0,0 +1,268 @@
1
+ <!doctype html>
2
+ <html lang="de">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
6
+ <title>JetFrame</title>
7
+ <link rel="manifest" href="manifest.webmanifest">
8
+ <link rel="apple-touch-icon" href="jetframe.png">
9
+ <link rel="icon" type="image/png" href="jetframe.png">
10
+ <meta name="apple-mobile-web-app-capable" content="yes">
11
+ <meta name="mobile-web-app-capable" content="yes">
12
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
13
+ <meta name="theme-color" content="#050709">
14
+ <link rel="stylesheet" href="jetframe.css?v=20260522-fix14">
15
+ </head>
16
+ <body class="jf-page-home">
17
+
18
+ <div id="simpleApiWarning" class="simpleApiWarning">
19
+ ⚠️ Simple-API nicht erreichbar<br>
20
+ <span id="simpleApiWarningText"></span>
21
+ </div>
22
+
23
+ <div id="app">
24
+ <div class="jf-shell">
25
+ <div class="hero">
26
+ <div class="logo">✈️</div>
27
+ <h1>JetFrame</h1>
28
+ <div class="sub">Dein Live-Flugradar für Zuhause</div>
29
+ <div id="statusText" class="statusPill">Verbinde mit JetFrame…</div>
30
+ </div>
31
+
32
+ <div class="buttons">
33
+ <div id="globalPowerBtn" class="powerBtn on" role="button" tabindex="0" aria-label="JetFrame Ein/Aus">
34
+ <div>
35
+ <div class="btnTitle">JetFrame aktiv</div>
36
+ <div id="globalPowerSub" class="btnSub">Live-Erkennung ist eingeschaltet</div>
37
+ </div>
38
+ <div id="globalPowerState" class="powerBadge">ON</div>
39
+ </div>
40
+
41
+ <a id="frameLink" href="frame.html">
42
+ <div class="btn primary">
43
+ <div>
44
+ <div class="btnTitle">Live Frame</div>
45
+ <div class="btnSub">Aktuelles Flugzeug anzeigen</div>
46
+ </div>
47
+ <div class="arrow">›</div>
48
+ </div>
49
+ </a>
50
+
51
+ <a id="heatmapLink" href="heatmap.html">
52
+ <div class="btn heat">
53
+ <div>
54
+ <div class="btnTitle">Heatmap</div>
55
+ <div class="btnSub">Tagesstatistik & beste Spotterzeit</div>
56
+ </div>
57
+ <div class="arrow">›</div>
58
+ </div>
59
+ </a>
60
+
61
+ <a id="statsLink" href="stats.html">
62
+ <div class="btn">
63
+ <div>
64
+ <div class="btnTitle">Statistik</div>
65
+ <div class="btnSub">Gestern, Rekorde & Spotterzeiten</div>
66
+ </div>
67
+ <div class="arrow">›</div>
68
+ </div>
69
+ </a>
70
+ </div>
71
+
72
+ <div class="statsRow">
73
+ <div class="statCard">
74
+ <div id="totalFlights" class="statVal">–</div>
75
+ <div class="statLabel">HEUTE</div>
76
+ </div>
77
+ <div class="statCard">
78
+ <div id="currentHour" class="statVal">–</div>
79
+ <div class="statLabel">AKTUELL</div>
80
+ </div>
81
+ <div class="statCard">
82
+ <div id="landings" class="statVal">–</div>
83
+ <div class="statLabel">LANDUNGEN</div>
84
+ </div>
85
+ </div>
86
+
87
+ <div class="footer">JetFrame · Auto-Refresh alle 10 Sekunden</div>
88
+ </div>
89
+ </div>
90
+
91
+ <script>
92
+ 'use strict';
93
+
94
+ let API_BASE = (() => {
95
+ const proto = window.location.protocol || 'http:';
96
+ const host = window.location.hostname || '127.0.0.1';
97
+ const url = new URL(window.location.href);
98
+ const apiHost = url.searchParams.get('apiHost') || host;
99
+ const apiPort = url.searchParams.get('apiPort') || '8087';
100
+ return proto + '//' + apiHost + ':' + apiPort;
101
+ })();
102
+
103
+ const DP_ROOT = (() => {
104
+ const url = new URL(window.location.href);
105
+ const instance = url.searchParams.get('instance') || '0';
106
+ return 'jetframe.' + String(instance).replace(/[^0-9]/g, '');
107
+ })();
108
+
109
+ let VIS_CONFIG_LOADED = false;
110
+
111
+ function $(id) { return document.getElementById(id); }
112
+
113
+ function clean(v) {
114
+ if (v === null || v === undefined) return '';
115
+ v = String(v).trim();
116
+ if (v === 'null' || v === 'undefined') return '';
117
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
118
+ v = v.slice(1, -1);
119
+ }
120
+ return v.replace(/\\"/g, '"').replace(/\\n/g, '').trim();
121
+ }
122
+
123
+ async function loadVisualConfig() {
124
+ const url = new URL(window.location.href);
125
+ const hasApiOverride = url.searchParams.has('apiHost') || url.searchParams.has('apiPort');
126
+
127
+ try {
128
+ const res = await fetch('/jetframe.admin/vis-config.json?v=' + Date.now(), { cache: 'no-store' });
129
+ if (!res.ok) throw new Error('vis-config.json nicht gefunden');
130
+
131
+ const cfg = await res.json();
132
+
133
+ if (!hasApiOverride) {
134
+ const proto = window.location.protocol || 'http:';
135
+ const host = String(cfg.simpleApiHost || window.location.hostname || '127.0.0.1').trim();
136
+ const port = String(cfg.simpleApiPort || '8087').trim();
137
+ API_BASE = proto + '//' + host + ':' + port;
138
+ }
139
+
140
+ console.log('[JetFrame] vis-config.json geladen:', cfg, API_BASE);
141
+ } catch (e) {
142
+ console.warn('[JetFrame] vis-config.json nicht geladen, nutze Fallback:', API_BASE, e);
143
+ }
144
+
145
+ VIS_CONFIG_LOADED = true;
146
+ }
147
+
148
+ function setSimpleApiWarning(visible, text) {
149
+ const box = $('simpleApiWarning');
150
+ const msg = $('simpleApiWarningText');
151
+ if (msg) msg.textContent = text || API_BASE;
152
+ if (box) box.classList.toggle('visible', !!visible);
153
+ }
154
+
155
+ async function checkSimpleApiReachable() {
156
+ try {
157
+ const res = await fetch(API_BASE + '/getPlainValue/' + encodeURIComponent(DP_ROOT + '.status'), { cache: 'no-store' });
158
+ if (!res.ok) throw new Error('HTTP ' + res.status);
159
+ setSimpleApiWarning(false, '');
160
+ return true;
161
+ } catch (e) {
162
+ setSimpleApiWarning(true, API_BASE);
163
+ return false;
164
+ }
165
+ }
166
+
167
+ async function readState(id) {
168
+ if (!VIS_CONFIG_LOADED) return '';
169
+ try {
170
+ const res = await fetch(API_BASE + '/getPlainValue/' + encodeURIComponent(id), { cache: 'no-store' });
171
+ if (!res.ok) return '';
172
+ return clean(await res.text());
173
+ } catch (e) {
174
+ setSimpleApiWarning(true, API_BASE);
175
+ return '';
176
+ }
177
+ }
178
+
179
+ async function writeState(id, value) {
180
+ try {
181
+ await fetch(API_BASE + '/set/' + encodeURIComponent(id) + '?value=' + encodeURIComponent(value), { cache: 'no-store' });
182
+ } catch (e) {
183
+ setSimpleApiWarning(true, API_BASE);
184
+ }
185
+ }
186
+
187
+ function setGlobalPowerButton(enabled) {
188
+ const btn = $('globalPowerBtn');
189
+ const state = $('globalPowerState');
190
+ const sub = $('globalPowerSub');
191
+ const title = btn ? btn.querySelector('.btnTitle') : null;
192
+
193
+ if (!btn) return;
194
+
195
+ btn.classList.toggle('on', !!enabled); btn.classList.toggle('off', !enabled);
196
+ btn.classList.toggle('off', !enabled);
197
+
198
+ if (title) title.textContent = enabled ? 'JetFrame aktiv' : 'JetFrame pausiert';
199
+ if (state) state.textContent = enabled ? 'ON' : 'OFF';
200
+ if (sub) sub.textContent = enabled ? 'Live-Erkennung ist eingeschaltet' : 'Live-Erkennung ist pausiert';
201
+ }
202
+
203
+ function applyQueryLinks() {
204
+ const q = window.location.search || '';
205
+ if ($('frameLink')) $('frameLink').href = 'frame.html' + q;
206
+ if ($('heatmapLink')) $('heatmapLink').href = 'heatmap.html' + q;
207
+ if ($('statsLink')) $('statsLink').href = 'stats.html' + q;
208
+ }
209
+
210
+ async function poll() {
211
+ await checkSimpleApiReachable();
212
+
213
+ const base = DP_ROOT + '.statistics';
214
+
215
+ const [status, enabled, total, current, landings, runway] = await Promise.all([
216
+ readState(DP_ROOT + '.status'),
217
+ readState(DP_ROOT + '.enabled'),
218
+ readState(base + '.today.totalFlights'),
219
+ readState(base + '.today.currentHourFlights'),
220
+ readState(base + '.today.landings'),
221
+ readState(DP_ROOT + '.idleRunwayText'),
222
+ ]);
223
+
224
+ if ($('totalFlights')) $('totalFlights').textContent = Number(total || 0);
225
+ if ($('currentHour')) $('currentHour').textContent = Number(current || 0);
226
+ if ($('landings')) $('landings').textContent = Number(landings || 0);
227
+
228
+ setGlobalPowerButton(clean(enabled) !== 'false');
229
+
230
+ const parts = [];
231
+ if (status) parts.push('Status: ' + status);
232
+ if (runway) parts.push(runway);
233
+ if ($('statusText')) $('statusText').textContent = parts.length ? parts.join(' · ') : 'JetFrame bereit';
234
+ }
235
+
236
+ async function toggleGlobalPower() {
237
+ const current = clean(await readState(DP_ROOT + '.enabled')) !== 'false';
238
+ const next = !current;
239
+ await writeState(DP_ROOT + '.enabled', next ? 'true' : 'false');
240
+ setGlobalPowerButton(next);
241
+ poll();
242
+ }
243
+
244
+ const powerBtn = $('globalPowerBtn');
245
+ if (powerBtn) {
246
+ powerBtn.addEventListener('click', toggleGlobalPower);
247
+ powerBtn.addEventListener('keydown', e => {
248
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleGlobalPower(); }
249
+ });
250
+ }
251
+
252
+ applyQueryLinks();
253
+
254
+ loadVisualConfig()
255
+ .then(() => {
256
+ poll();
257
+ setTimeout(poll, 2500);
258
+ setInterval(poll, 10000);
259
+ })
260
+ .catch(e => {
261
+ console.error('[JetFrame] Start abgebrochen:', e);
262
+ VIS_CONFIG_LOADED = true;
263
+ poll();
264
+ setInterval(poll, 10000);
265
+ });
266
+ </script>
267
+ </body>
268
+ </html>