gameglue 4.0.0 → 4.0.2

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 (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +275 -275
  3. package/babel.config.cjs +5 -5
  4. package/coverage/auth.js.html +525 -525
  5. package/coverage/base.css +224 -224
  6. package/coverage/block-navigation.js +87 -87
  7. package/coverage/favicon.png +0 -0
  8. package/coverage/index.html +175 -175
  9. package/coverage/index.js.html +309 -309
  10. package/coverage/lcov-report/auth.js.html +525 -525
  11. package/coverage/lcov-report/base.css +224 -224
  12. package/coverage/lcov-report/block-navigation.js +87 -87
  13. package/coverage/lcov-report/favicon.png +0 -0
  14. package/coverage/lcov-report/index.html +175 -175
  15. package/coverage/lcov-report/index.js.html +309 -309
  16. package/coverage/lcov-report/listener.js.html +528 -528
  17. package/coverage/lcov-report/prettify.css +1 -1
  18. package/coverage/lcov-report/prettify.js +2 -2
  19. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  20. package/coverage/lcov-report/sorter.js +210 -210
  21. package/coverage/lcov-report/user.js.html +117 -117
  22. package/coverage/lcov-report/utils.js.html +117 -117
  23. package/coverage/lcov.info +391 -391
  24. package/coverage/listener.js.html +528 -528
  25. package/coverage/prettify.css +1 -1
  26. package/coverage/prettify.js +2 -2
  27. package/coverage/sort-arrow-sprite.png +0 -0
  28. package/coverage/sorter.js +210 -210
  29. package/coverage/user.js.html +117 -117
  30. package/coverage/utils.js.html +117 -117
  31. package/dist/gg.cjs.js +1 -1
  32. package/dist/gg.cjs.js.map +1 -1
  33. package/dist/gg.esm.js +1 -1
  34. package/dist/gg.esm.js.map +1 -1
  35. package/dist/gg.umd.js +1 -1
  36. package/dist/gg.umd.js.map +1 -1
  37. package/examples/certs/cert.pem +19 -19
  38. package/examples/certs/key.pem +28 -28
  39. package/examples/flight-dashboard.html +431 -431
  40. package/examples/server.js +99 -99
  41. package/examples/telemetry-validator.html +1410 -1410
  42. package/jest.config.cjs +33 -33
  43. package/package.json +56 -56
  44. package/rollup.config.js +57 -57
  45. package/src/auth.js +255 -255
  46. package/src/auth.spec.js +481 -481
  47. package/src/index.js +168 -168
  48. package/src/listener.js +196 -193
  49. package/src/listener.spec.js +598 -598
  50. package/src/presence_listener.js +112 -112
  51. package/src/test/fixtures.js +106 -106
  52. package/src/test/setup.js +51 -51
  53. package/src/utils.js +63 -63
  54. package/src/utils.spec.js +78 -78
  55. package/types/index.d.ts +338 -338
  56. package/webpack.config.js +15 -15
@@ -1,431 +1,431 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>GameGlue Flight Dashboard Example</title>
7
- <style>
8
- * {
9
- box-sizing: border-box;
10
- margin: 0;
11
- padding: 0;
12
- }
13
- body {
14
- font-family: 'Segoe UI', -apple-system, sans-serif;
15
- background: #0f172a;
16
- color: #e2e8f0;
17
- min-height: 100vh;
18
- padding: 2rem;
19
- }
20
- .container {
21
- max-width: 800px;
22
- margin: 0 auto;
23
- }
24
- h1 {
25
- font-size: 1.5rem;
26
- margin-bottom: 0.5rem;
27
- color: #3cff8f;
28
- }
29
- .subtitle {
30
- color: #64748b;
31
- margin-bottom: 2rem;
32
- }
33
- .header {
34
- display: flex;
35
- justify-content: space-between;
36
- align-items: center;
37
- margin-bottom: 1.5rem;
38
- }
39
- .status {
40
- display: inline-block;
41
- padding: 0.25rem 0.75rem;
42
- border-radius: 9999px;
43
- font-size: 0.875rem;
44
- }
45
- .status.disconnected { background: #7f1d1d; color: #fca5a5; }
46
- .status.connecting { background: #78350f; color: #fcd34d; }
47
- .status.connected { background: #14532d; color: #86efac; }
48
-
49
- .logout-btn {
50
- background: transparent;
51
- border: 1px solid #334155;
52
- color: #94a3b8;
53
- padding: 0.5rem 1rem;
54
- border-radius: 0.5rem;
55
- font-size: 0.875rem;
56
- cursor: pointer;
57
- transition: all 0.15s;
58
- }
59
- .logout-btn:hover {
60
- background: #334155;
61
- color: #f1f5f9;
62
- }
63
-
64
- .card {
65
- background: #1e293b;
66
- border-radius: 0.75rem;
67
- padding: 1.5rem;
68
- margin-bottom: 1.5rem;
69
- }
70
- .card h2 {
71
- font-size: 1rem;
72
- color: #94a3b8;
73
- margin-bottom: 1rem;
74
- text-transform: uppercase;
75
- letter-spacing: 0.05em;
76
- }
77
- .telemetry-grid {
78
- display: grid;
79
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
80
- gap: 1rem;
81
- }
82
- .telemetry-item {
83
- background: #0f172a;
84
- padding: 1rem;
85
- border-radius: 0.5rem;
86
- }
87
- .telemetry-label {
88
- font-size: 0.75rem;
89
- color: #64748b;
90
- text-transform: uppercase;
91
- margin-bottom: 0.25rem;
92
- }
93
- .telemetry-value {
94
- font-size: 1.5rem;
95
- font-weight: 600;
96
- font-family: 'SF Mono', 'Consolas', monospace;
97
- }
98
- .telemetry-unit {
99
- font-size: 0.875rem;
100
- color: #64748b;
101
- margin-left: 0.25rem;
102
- }
103
-
104
- .controls-grid {
105
- display: grid;
106
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
107
- gap: 1rem;
108
- }
109
- .control-btn {
110
- background: #334155;
111
- border: none;
112
- color: #e2e8f0;
113
- padding: 1rem;
114
- border-radius: 0.5rem;
115
- font-size: 0.875rem;
116
- cursor: pointer;
117
- transition: all 0.15s;
118
- display: flex;
119
- flex-direction: column;
120
- align-items: center;
121
- gap: 0.5rem;
122
- }
123
- .control-btn:hover {
124
- background: #475569;
125
- }
126
- .control-btn:active {
127
- transform: scale(0.98);
128
- }
129
- .control-btn.active {
130
- background: #166534;
131
- color: #86efac;
132
- }
133
- .control-btn .icon {
134
- font-size: 1.5rem;
135
- }
136
-
137
- .auth-section {
138
- text-align: center;
139
- padding: 3rem;
140
- }
141
- .auth-btn {
142
- background: #3cff8f;
143
- color: #0f172a;
144
- border: none;
145
- padding: 0.75rem 2rem;
146
- border-radius: 0.5rem;
147
- font-size: 1rem;
148
- font-weight: 600;
149
- cursor: pointer;
150
- transition: all 0.15s;
151
- }
152
- .auth-btn:hover {
153
- background: #22c55e;
154
- }
155
-
156
- .log {
157
- background: #0f172a;
158
- border-radius: 0.5rem;
159
- padding: 1rem;
160
- font-family: 'SF Mono', 'Consolas', monospace;
161
- font-size: 0.75rem;
162
- max-height: 200px;
163
- overflow-y: auto;
164
- }
165
- .log-entry {
166
- color: #64748b;
167
- margin-bottom: 0.25rem;
168
- }
169
- .log-entry.command {
170
- color: #fcd34d;
171
- }
172
- .log-entry.error {
173
- color: #f87171;
174
- }
175
-
176
- .hidden { display: none; }
177
- </style>
178
- </head>
179
- <body>
180
- <div class="container">
181
- <h1>/GameGlue Flight Dashboard</h1>
182
- <p class="subtitle">Example app demonstrating bidirectional data flow</p>
183
-
184
- <!-- Auth Section (shown when not authenticated) -->
185
- <div id="auth-section" class="card auth-section">
186
- <p style="margin-bottom: 1rem; color: #94a3b8;">Connect to GameGlue to view flight telemetry and send commands</p>
187
- <button class="auth-btn" onclick="doLogin()">Connect with GameGlue</button>
188
- </div>
189
-
190
- <!-- Dashboard (shown when authenticated) -->
191
- <div id="dashboard" class="hidden">
192
- <div class="header">
193
- <span id="status" class="status disconnected">Waiting for MSFS...</span>
194
- <button class="logout-btn" onclick="doLogout()">Logout</button>
195
- </div>
196
-
197
- <!-- Telemetry Card -->
198
- <div class="card">
199
- <h2>Flight Data</h2>
200
- <div class="telemetry-grid">
201
- <div class="telemetry-item">
202
- <div class="telemetry-label">Altitude</div>
203
- <div>
204
- <span id="altitude" class="telemetry-value">---</span>
205
- <span class="telemetry-unit">ft</span>
206
- </div>
207
- </div>
208
- <div class="telemetry-item">
209
- <div class="telemetry-label">Airspeed</div>
210
- <div>
211
- <span id="airspeed" class="telemetry-value">---</span>
212
- <span class="telemetry-unit">kts</span>
213
- </div>
214
- </div>
215
- <div class="telemetry-item">
216
- <div class="telemetry-label">Ground Speed</div>
217
- <div>
218
- <span id="ground-speed" class="telemetry-value">---</span>
219
- <span class="telemetry-unit">kts</span>
220
- </div>
221
- </div>
222
- <div class="telemetry-item">
223
- <div class="telemetry-label">Vertical Speed</div>
224
- <div>
225
- <span id="vertical-speed" class="telemetry-value">---</span>
226
- <span class="telemetry-unit">fpm</span>
227
- </div>
228
- </div>
229
- <div class="telemetry-item">
230
- <div class="telemetry-label">Heading</div>
231
- <div>
232
- <span id="heading" class="telemetry-value">---</span>
233
- <span class="telemetry-unit">&deg;</span>
234
- </div>
235
- </div>
236
- <div class="telemetry-item">
237
- <div class="telemetry-label">Autopilot</div>
238
- <div>
239
- <span id="autopilot-status" class="telemetry-value">---</span>
240
- </div>
241
- </div>
242
- </div>
243
- </div>
244
-
245
- <!-- Controls Card -->
246
- <div class="card">
247
- <h2>Aircraft Controls</h2>
248
- <div class="controls-grid">
249
- <button class="control-btn" onclick="sendCommand('AUTOPILOT_ON', true)">
250
- <span class="icon">AP</span>
251
- <span>Autopilot ON</span>
252
- </button>
253
- <button class="control-btn" onclick="sendCommand('AUTOPILOT_OFF', true)">
254
- <span class="icon">AP</span>
255
- <span>Autopilot OFF</span>
256
- </button>
257
- <button class="control-btn" onclick="sendCommand('TOGGLE_FLIGHT_DIRECTOR', true)">
258
- <span class="icon">FD</span>
259
- <span>Toggle Flight Director</span>
260
- </button>
261
- <button class="control-btn" onclick="sendCommand('AP_HDG_HOLD_ON', true)">
262
- <span class="icon">HDG</span>
263
- <span>Heading Hold ON</span>
264
- </button>
265
- <button class="control-btn" onclick="sendCommand('AP_ALT_HOLD_ON', true)">
266
- <span class="icon">ALT</span>
267
- <span>Altitude Hold ON</span>
268
- </button>
269
- <button class="control-btn" onclick="sendCommand('TOGGLE_NAV_LIGHTS', true)">
270
- <span class="icon">NAV</span>
271
- <span>Toggle Nav Lights</span>
272
- </button>
273
- </div>
274
- </div>
275
-
276
- <!-- Log Card -->
277
- <div class="card">
278
- <h2>Event Log</h2>
279
- <div id="log" class="log"></div>
280
- </div>
281
- </div>
282
- </div>
283
-
284
- <!-- Load GameGlue SDK from local build -->
285
- <script src="../dist/gg.umd.js"></script>
286
- <script>
287
- // Configuration
288
- const CLIENT_ID = 'gameglue-sdk-examples';
289
- const REDIRECT_URI = window.location.href.split('?')[0].split('#')[0];
290
-
291
- // Create a single SDK instance
292
- const ggClient = new GameGlue({
293
- clientId: CLIENT_ID,
294
- redirect_uri: REDIRECT_URI,
295
- scopes: ['msfs:read', 'msfs:write']
296
- });
297
-
298
- let listener = null;
299
-
300
- // Login button handler
301
- function doLogin() {
302
- log('Redirecting to GameGlue login...');
303
- ggClient.login();
304
- }
305
-
306
- // Logout button handler
307
- function doLogout() {
308
- log('Logging out...');
309
- ggClient.logout({ redirect: false });
310
- document.getElementById('auth-section').classList.remove('hidden');
311
- document.getElementById('dashboard').classList.add('hidden');
312
- }
313
-
314
- // Check authentication and set up listener
315
- async function init() {
316
- try {
317
- log('Checking authentication...');
318
-
319
- // isAuthenticated() handles OAuth callback automatically
320
- const isAuthed = await ggClient.isAuthenticated();
321
-
322
- if (!isAuthed) {
323
- log('Not authenticated - please log in');
324
- return;
325
- }
326
-
327
- // Authenticated
328
- const userId = ggClient.getUser();
329
- log('Authenticated! User ID: ' + userId);
330
- showDashboard();
331
- await setupListener(userId);
332
- } catch (err) {
333
- log('Error: ' + err.message, 'error');
334
- }
335
- }
336
-
337
- // Set up telemetry listener
338
- async function setupListener(userId) {
339
- try {
340
- log('Creating listener for MSFS telemetry...');
341
-
342
- listener = await ggClient.createListener({
343
- userId: userId,
344
- gameId: 'msfs'
345
- });
346
-
347
- listener.on('update', (evt) => {
348
- updateTelemetry(evt.data);
349
- });
350
-
351
- setStatus('connected', 'Connected to MSFS');
352
- log('Listener active. Waiting for telemetry...');
353
- } catch (err) {
354
- log('Failed to create listener: ' + err.message, 'error');
355
- setStatus('disconnected', 'Connection failed');
356
- }
357
- }
358
-
359
- // Update telemetry display
360
- function updateTelemetry(data) {
361
- if (data.indicated_altitude !== undefined) {
362
- document.getElementById('altitude').textContent = Math.round(data.indicated_altitude);
363
- }
364
- if (data.airspeed_indicated !== undefined) {
365
- document.getElementById('airspeed').textContent = Math.round(data.airspeed_indicated);
366
- }
367
- if (data.ground_velocity !== undefined) {
368
- document.getElementById('ground-speed').textContent = Math.round(data.ground_velocity);
369
- }
370
- if (data.vertical_speed !== undefined) {
371
- document.getElementById('vertical-speed').textContent = Math.round(data.vertical_speed);
372
- }
373
- if (data.heading_indicator !== undefined) {
374
- document.getElementById('heading').textContent = Math.round(data.heading_indicator);
375
- }
376
- if (data.autopilot_master !== undefined) {
377
- document.getElementById('autopilot-status').textContent = data.autopilot_master ? 'ON' : 'OFF';
378
- document.getElementById('autopilot-status').style.color = data.autopilot_master ? '#86efac' : '#f87171';
379
- }
380
- }
381
-
382
- // Send command to aircraft
383
- async function sendCommand(field, value) {
384
- if (!listener) {
385
- log('Not connected - cannot send command', 'error');
386
- return;
387
- }
388
-
389
- log(`Sending command: ${field} = ${value}`, 'command');
390
-
391
- try {
392
- const result = await listener.sendCommand(field, value);
393
- if (result.status === 'success') {
394
- log(`Command sent successfully: ${field}`, 'command');
395
- } else {
396
- log(`Command failed: ${result.reason}`, 'error');
397
- }
398
- } catch (err) {
399
- log(`Command error: ${err.message}`, 'error');
400
- }
401
- }
402
-
403
- // UI Helpers
404
- function showDashboard() {
405
- document.getElementById('auth-section').classList.add('hidden');
406
- document.getElementById('dashboard').classList.remove('hidden');
407
- }
408
-
409
- function setStatus(state, text) {
410
- const el = document.getElementById('status');
411
- el.className = 'status ' + state;
412
- el.textContent = text;
413
- }
414
-
415
- function log(message, type = '') {
416
- const logEl = document.getElementById('log');
417
- if (logEl) {
418
- const entry = document.createElement('div');
419
- entry.className = 'log-entry ' + type;
420
- entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
421
- logEl.appendChild(entry);
422
- logEl.scrollTop = logEl.scrollHeight;
423
- }
424
- console.log(message);
425
- }
426
-
427
- // Initialize on page load
428
- window.onload = init;
429
- </script>
430
- </body>
431
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GameGlue Flight Dashboard Example</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+ body {
14
+ font-family: 'Segoe UI', -apple-system, sans-serif;
15
+ background: #0f172a;
16
+ color: #e2e8f0;
17
+ min-height: 100vh;
18
+ padding: 2rem;
19
+ }
20
+ .container {
21
+ max-width: 800px;
22
+ margin: 0 auto;
23
+ }
24
+ h1 {
25
+ font-size: 1.5rem;
26
+ margin-bottom: 0.5rem;
27
+ color: #3cff8f;
28
+ }
29
+ .subtitle {
30
+ color: #64748b;
31
+ margin-bottom: 2rem;
32
+ }
33
+ .header {
34
+ display: flex;
35
+ justify-content: space-between;
36
+ align-items: center;
37
+ margin-bottom: 1.5rem;
38
+ }
39
+ .status {
40
+ display: inline-block;
41
+ padding: 0.25rem 0.75rem;
42
+ border-radius: 9999px;
43
+ font-size: 0.875rem;
44
+ }
45
+ .status.disconnected { background: #7f1d1d; color: #fca5a5; }
46
+ .status.connecting { background: #78350f; color: #fcd34d; }
47
+ .status.connected { background: #14532d; color: #86efac; }
48
+
49
+ .logout-btn {
50
+ background: transparent;
51
+ border: 1px solid #334155;
52
+ color: #94a3b8;
53
+ padding: 0.5rem 1rem;
54
+ border-radius: 0.5rem;
55
+ font-size: 0.875rem;
56
+ cursor: pointer;
57
+ transition: all 0.15s;
58
+ }
59
+ .logout-btn:hover {
60
+ background: #334155;
61
+ color: #f1f5f9;
62
+ }
63
+
64
+ .card {
65
+ background: #1e293b;
66
+ border-radius: 0.75rem;
67
+ padding: 1.5rem;
68
+ margin-bottom: 1.5rem;
69
+ }
70
+ .card h2 {
71
+ font-size: 1rem;
72
+ color: #94a3b8;
73
+ margin-bottom: 1rem;
74
+ text-transform: uppercase;
75
+ letter-spacing: 0.05em;
76
+ }
77
+ .telemetry-grid {
78
+ display: grid;
79
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
80
+ gap: 1rem;
81
+ }
82
+ .telemetry-item {
83
+ background: #0f172a;
84
+ padding: 1rem;
85
+ border-radius: 0.5rem;
86
+ }
87
+ .telemetry-label {
88
+ font-size: 0.75rem;
89
+ color: #64748b;
90
+ text-transform: uppercase;
91
+ margin-bottom: 0.25rem;
92
+ }
93
+ .telemetry-value {
94
+ font-size: 1.5rem;
95
+ font-weight: 600;
96
+ font-family: 'SF Mono', 'Consolas', monospace;
97
+ }
98
+ .telemetry-unit {
99
+ font-size: 0.875rem;
100
+ color: #64748b;
101
+ margin-left: 0.25rem;
102
+ }
103
+
104
+ .controls-grid {
105
+ display: grid;
106
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
107
+ gap: 1rem;
108
+ }
109
+ .control-btn {
110
+ background: #334155;
111
+ border: none;
112
+ color: #e2e8f0;
113
+ padding: 1rem;
114
+ border-radius: 0.5rem;
115
+ font-size: 0.875rem;
116
+ cursor: pointer;
117
+ transition: all 0.15s;
118
+ display: flex;
119
+ flex-direction: column;
120
+ align-items: center;
121
+ gap: 0.5rem;
122
+ }
123
+ .control-btn:hover {
124
+ background: #475569;
125
+ }
126
+ .control-btn:active {
127
+ transform: scale(0.98);
128
+ }
129
+ .control-btn.active {
130
+ background: #166534;
131
+ color: #86efac;
132
+ }
133
+ .control-btn .icon {
134
+ font-size: 1.5rem;
135
+ }
136
+
137
+ .auth-section {
138
+ text-align: center;
139
+ padding: 3rem;
140
+ }
141
+ .auth-btn {
142
+ background: #3cff8f;
143
+ color: #0f172a;
144
+ border: none;
145
+ padding: 0.75rem 2rem;
146
+ border-radius: 0.5rem;
147
+ font-size: 1rem;
148
+ font-weight: 600;
149
+ cursor: pointer;
150
+ transition: all 0.15s;
151
+ }
152
+ .auth-btn:hover {
153
+ background: #22c55e;
154
+ }
155
+
156
+ .log {
157
+ background: #0f172a;
158
+ border-radius: 0.5rem;
159
+ padding: 1rem;
160
+ font-family: 'SF Mono', 'Consolas', monospace;
161
+ font-size: 0.75rem;
162
+ max-height: 200px;
163
+ overflow-y: auto;
164
+ }
165
+ .log-entry {
166
+ color: #64748b;
167
+ margin-bottom: 0.25rem;
168
+ }
169
+ .log-entry.command {
170
+ color: #fcd34d;
171
+ }
172
+ .log-entry.error {
173
+ color: #f87171;
174
+ }
175
+
176
+ .hidden { display: none; }
177
+ </style>
178
+ </head>
179
+ <body>
180
+ <div class="container">
181
+ <h1>/GameGlue Flight Dashboard</h1>
182
+ <p class="subtitle">Example app demonstrating bidirectional data flow</p>
183
+
184
+ <!-- Auth Section (shown when not authenticated) -->
185
+ <div id="auth-section" class="card auth-section">
186
+ <p style="margin-bottom: 1rem; color: #94a3b8;">Connect to GameGlue to view flight telemetry and send commands</p>
187
+ <button class="auth-btn" onclick="doLogin()">Connect with GameGlue</button>
188
+ </div>
189
+
190
+ <!-- Dashboard (shown when authenticated) -->
191
+ <div id="dashboard" class="hidden">
192
+ <div class="header">
193
+ <span id="status" class="status disconnected">Waiting for MSFS...</span>
194
+ <button class="logout-btn" onclick="doLogout()">Logout</button>
195
+ </div>
196
+
197
+ <!-- Telemetry Card -->
198
+ <div class="card">
199
+ <h2>Flight Data</h2>
200
+ <div class="telemetry-grid">
201
+ <div class="telemetry-item">
202
+ <div class="telemetry-label">Altitude</div>
203
+ <div>
204
+ <span id="altitude" class="telemetry-value">---</span>
205
+ <span class="telemetry-unit">ft</span>
206
+ </div>
207
+ </div>
208
+ <div class="telemetry-item">
209
+ <div class="telemetry-label">Airspeed</div>
210
+ <div>
211
+ <span id="airspeed" class="telemetry-value">---</span>
212
+ <span class="telemetry-unit">kts</span>
213
+ </div>
214
+ </div>
215
+ <div class="telemetry-item">
216
+ <div class="telemetry-label">Ground Speed</div>
217
+ <div>
218
+ <span id="ground-speed" class="telemetry-value">---</span>
219
+ <span class="telemetry-unit">kts</span>
220
+ </div>
221
+ </div>
222
+ <div class="telemetry-item">
223
+ <div class="telemetry-label">Vertical Speed</div>
224
+ <div>
225
+ <span id="vertical-speed" class="telemetry-value">---</span>
226
+ <span class="telemetry-unit">fpm</span>
227
+ </div>
228
+ </div>
229
+ <div class="telemetry-item">
230
+ <div class="telemetry-label">Heading</div>
231
+ <div>
232
+ <span id="heading" class="telemetry-value">---</span>
233
+ <span class="telemetry-unit">&deg;</span>
234
+ </div>
235
+ </div>
236
+ <div class="telemetry-item">
237
+ <div class="telemetry-label">Autopilot</div>
238
+ <div>
239
+ <span id="autopilot-status" class="telemetry-value">---</span>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </div>
244
+
245
+ <!-- Controls Card -->
246
+ <div class="card">
247
+ <h2>Aircraft Controls</h2>
248
+ <div class="controls-grid">
249
+ <button class="control-btn" onclick="sendCommand('AUTOPILOT_ON', true)">
250
+ <span class="icon">AP</span>
251
+ <span>Autopilot ON</span>
252
+ </button>
253
+ <button class="control-btn" onclick="sendCommand('AUTOPILOT_OFF', true)">
254
+ <span class="icon">AP</span>
255
+ <span>Autopilot OFF</span>
256
+ </button>
257
+ <button class="control-btn" onclick="sendCommand('TOGGLE_FLIGHT_DIRECTOR', true)">
258
+ <span class="icon">FD</span>
259
+ <span>Toggle Flight Director</span>
260
+ </button>
261
+ <button class="control-btn" onclick="sendCommand('AP_HDG_HOLD_ON', true)">
262
+ <span class="icon">HDG</span>
263
+ <span>Heading Hold ON</span>
264
+ </button>
265
+ <button class="control-btn" onclick="sendCommand('AP_ALT_HOLD_ON', true)">
266
+ <span class="icon">ALT</span>
267
+ <span>Altitude Hold ON</span>
268
+ </button>
269
+ <button class="control-btn" onclick="sendCommand('TOGGLE_NAV_LIGHTS', true)">
270
+ <span class="icon">NAV</span>
271
+ <span>Toggle Nav Lights</span>
272
+ </button>
273
+ </div>
274
+ </div>
275
+
276
+ <!-- Log Card -->
277
+ <div class="card">
278
+ <h2>Event Log</h2>
279
+ <div id="log" class="log"></div>
280
+ </div>
281
+ </div>
282
+ </div>
283
+
284
+ <!-- Load GameGlue SDK from local build -->
285
+ <script src="../dist/gg.umd.js"></script>
286
+ <script>
287
+ // Configuration
288
+ const CLIENT_ID = 'gameglue-sdk-examples';
289
+ const REDIRECT_URI = window.location.href.split('?')[0].split('#')[0];
290
+
291
+ // Create a single SDK instance
292
+ const ggClient = new GameGlue({
293
+ clientId: CLIENT_ID,
294
+ redirect_uri: REDIRECT_URI,
295
+ scopes: ['msfs:read', 'msfs:write']
296
+ });
297
+
298
+ let listener = null;
299
+
300
+ // Login button handler
301
+ function doLogin() {
302
+ log('Redirecting to GameGlue login...');
303
+ ggClient.login();
304
+ }
305
+
306
+ // Logout button handler
307
+ function doLogout() {
308
+ log('Logging out...');
309
+ ggClient.logout({ redirect: false });
310
+ document.getElementById('auth-section').classList.remove('hidden');
311
+ document.getElementById('dashboard').classList.add('hidden');
312
+ }
313
+
314
+ // Check authentication and set up listener
315
+ async function init() {
316
+ try {
317
+ log('Checking authentication...');
318
+
319
+ // isAuthenticated() handles OAuth callback automatically
320
+ const isAuthed = await ggClient.isAuthenticated();
321
+
322
+ if (!isAuthed) {
323
+ log('Not authenticated - please log in');
324
+ return;
325
+ }
326
+
327
+ // Authenticated
328
+ const userId = ggClient.getUser();
329
+ log('Authenticated! User ID: ' + userId);
330
+ showDashboard();
331
+ await setupListener(userId);
332
+ } catch (err) {
333
+ log('Error: ' + err.message, 'error');
334
+ }
335
+ }
336
+
337
+ // Set up telemetry listener
338
+ async function setupListener(userId) {
339
+ try {
340
+ log('Creating listener for MSFS telemetry...');
341
+
342
+ listener = await ggClient.createListener({
343
+ userId: userId,
344
+ gameId: 'msfs'
345
+ });
346
+
347
+ listener.on('update', (evt) => {
348
+ updateTelemetry(evt.data);
349
+ });
350
+
351
+ setStatus('connected', 'Connected to MSFS');
352
+ log('Listener active. Waiting for telemetry...');
353
+ } catch (err) {
354
+ log('Failed to create listener: ' + err.message, 'error');
355
+ setStatus('disconnected', 'Connection failed');
356
+ }
357
+ }
358
+
359
+ // Update telemetry display
360
+ function updateTelemetry(data) {
361
+ if (data.indicated_altitude !== undefined) {
362
+ document.getElementById('altitude').textContent = Math.round(data.indicated_altitude);
363
+ }
364
+ if (data.airspeed_indicated !== undefined) {
365
+ document.getElementById('airspeed').textContent = Math.round(data.airspeed_indicated);
366
+ }
367
+ if (data.ground_velocity !== undefined) {
368
+ document.getElementById('ground-speed').textContent = Math.round(data.ground_velocity);
369
+ }
370
+ if (data.vertical_speed !== undefined) {
371
+ document.getElementById('vertical-speed').textContent = Math.round(data.vertical_speed);
372
+ }
373
+ if (data.heading_indicator !== undefined) {
374
+ document.getElementById('heading').textContent = Math.round(data.heading_indicator);
375
+ }
376
+ if (data.autopilot_master !== undefined) {
377
+ document.getElementById('autopilot-status').textContent = data.autopilot_master ? 'ON' : 'OFF';
378
+ document.getElementById('autopilot-status').style.color = data.autopilot_master ? '#86efac' : '#f87171';
379
+ }
380
+ }
381
+
382
+ // Send command to aircraft
383
+ async function sendCommand(field, value) {
384
+ if (!listener) {
385
+ log('Not connected - cannot send command', 'error');
386
+ return;
387
+ }
388
+
389
+ log(`Sending command: ${field} = ${value}`, 'command');
390
+
391
+ try {
392
+ const result = await listener.sendCommand(field, value);
393
+ if (result.status === 'success') {
394
+ log(`Command sent successfully: ${field}`, 'command');
395
+ } else {
396
+ log(`Command failed: ${result.reason}`, 'error');
397
+ }
398
+ } catch (err) {
399
+ log(`Command error: ${err.message}`, 'error');
400
+ }
401
+ }
402
+
403
+ // UI Helpers
404
+ function showDashboard() {
405
+ document.getElementById('auth-section').classList.add('hidden');
406
+ document.getElementById('dashboard').classList.remove('hidden');
407
+ }
408
+
409
+ function setStatus(state, text) {
410
+ const el = document.getElementById('status');
411
+ el.className = 'status ' + state;
412
+ el.textContent = text;
413
+ }
414
+
415
+ function log(message, type = '') {
416
+ const logEl = document.getElementById('log');
417
+ if (logEl) {
418
+ const entry = document.createElement('div');
419
+ entry.className = 'log-entry ' + type;
420
+ entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
421
+ logEl.appendChild(entry);
422
+ logEl.scrollTop = logEl.scrollHeight;
423
+ }
424
+ console.log(message);
425
+ }
426
+
427
+ // Initialize on page load
428
+ window.onload = init;
429
+ </script>
430
+ </body>
431
+ </html>