truenorth 0.5.1 → 0.6.1
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.
- checksums.yaml +4 -4
- data/lib/truenorth/browser_client.rb +284 -486
- data/lib/truenorth/cli.rb +3 -2
- data/lib/truenorth/client.rb +37 -15
- data/lib/truenorth/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf116b555b67f39c34c1579a5d1212ee86d26c33a2d1bc88bf681d6145728b2f
|
|
4
|
+
data.tar.gz: d6ed28ab2f74be19204f331e1c37a4c925c021c00689c33d0b0a56880abdb9b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '079000943acd8e2f5a8cd278c5c20f52fe400d2df7189e6baa9aa4b64c7c814fa07f614af06fb1c6251fa302b125f8d1beb3cfa84b58287fdb04b8265706df7b'
|
|
7
|
+
data.tar.gz: 77a071e50db72b2c239f6b1d1beba278ff8e728aea8cd955f9a73bdd13d4535493b1412e3527fc9ebeff9fc964a8dbcff31eae2f2bf9addbf77a523965004756
|
|
@@ -28,6 +28,9 @@ module Truenorth
|
|
|
28
28
|
)
|
|
29
29
|
log 'Browser started'
|
|
30
30
|
|
|
31
|
+
# Auto-accept any alert dialogs (e.g., "Please create reservation for the future")
|
|
32
|
+
@browser.on(:dialog) { |dialog| dialog.accept }
|
|
33
|
+
|
|
31
34
|
# Maximize window in debug mode
|
|
32
35
|
if @debug && @browser
|
|
33
36
|
@browser.resize(width: 1920, height: 1080)
|
|
@@ -118,451 +121,150 @@ module Truenorth
|
|
|
118
121
|
|
|
119
122
|
navigate_to_date_and_activity(date, activity)
|
|
120
123
|
|
|
121
|
-
# Find and click the slot
|
|
122
|
-
slot_info = find_and_click_slot(time, court)
|
|
124
|
+
# Find and click the slot (dispatches mousedown+mouseup, triggers reservation AJAX)
|
|
125
|
+
slot_info = find_and_click_slot(time, court, date)
|
|
123
126
|
raise BookingError, "No slot available at #{time}" unless slot_info
|
|
124
127
|
|
|
125
128
|
log "Clicked slot: #{slot_info[:court]} at #{slot_info[:time]}"
|
|
126
129
|
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
# Check URL after click
|
|
131
|
-
current_url = @browser.url
|
|
132
|
-
page_title = @browser.title
|
|
133
|
-
log "After slot click - URL: #{current_url}"
|
|
134
|
-
log "After slot click - Title: #{page_title}"
|
|
135
|
-
|
|
136
|
-
# Capture what dialog appeared (for debugging)
|
|
137
|
-
if @debug
|
|
138
|
-
dialog_info = @browser.evaluate(<<~JS)
|
|
139
|
-
(function() {
|
|
140
|
-
var result = {
|
|
141
|
-
dialogsFound: 0,
|
|
142
|
-
visibleDialogs: 0,
|
|
143
|
-
dialogHtml: null,
|
|
144
|
-
allDialogIds: []
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
var dialogs = document.querySelectorAll('.ui-dialog');
|
|
148
|
-
result.dialogsFound = dialogs.length;
|
|
149
|
-
|
|
150
|
-
for (var i = 0; i < dialogs.length; i++) {
|
|
151
|
-
var dialog = dialogs[i];
|
|
152
|
-
var content = dialog.querySelector('.ui-dialog-content');
|
|
153
|
-
result.allDialogIds.push(content ? content.id : 'no-id');
|
|
154
|
-
|
|
155
|
-
var style = window.getComputedStyle(dialog);
|
|
156
|
-
if (style.display !== 'none' && style.visibility !== 'hidden') {
|
|
157
|
-
result.visibleDialogs++;
|
|
158
|
-
if (!result.dialogHtml) {
|
|
159
|
-
result.dialogHtml = dialog.outerHTML;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return result;
|
|
165
|
-
})()
|
|
166
|
-
JS
|
|
167
|
-
|
|
168
|
-
log "Dialog capture info: #{dialog_info['dialogsFound']} total, #{dialog_info['visibleDialogs']} visible"
|
|
169
|
-
log "Dialog IDs: #{dialog_info['allDialogIds'].join(', ')}"
|
|
170
|
-
|
|
171
|
-
if dialog_info['dialogHtml']
|
|
172
|
-
File.write('/tmp/first_dialog.html', dialog_info['dialogHtml'])
|
|
173
|
-
log 'Saved first dialog HTML to /tmp/first_dialog.html'
|
|
174
|
-
else
|
|
175
|
-
log 'WARNING: No visible dialog HTML to capture!'
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
@browser.screenshot(path: '/tmp/first_dialog.png')
|
|
179
|
-
log "Saved first dialog screenshot (URL: #{@browser.url}, Title: #{@browser.title})"
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Close legends dialog if it appears
|
|
183
|
-
legends_closed = @browser.evaluate(<<~JS)
|
|
184
|
-
(function() {
|
|
185
|
-
var dialogs = document.querySelectorAll('.ui-dialog');
|
|
186
|
-
for (var i = 0; i < dialogs.length; i++) {
|
|
187
|
-
var dialog = dialogs[i];
|
|
188
|
-
if (dialog.style.display === 'none') continue;
|
|
189
|
-
var legendsContent = dialog.querySelector('[id*="legends_content"]');
|
|
190
|
-
if (legendsContent) {
|
|
191
|
-
var closeBtn = dialog.querySelector('a.cross');
|
|
192
|
-
if (closeBtn) {
|
|
193
|
-
closeBtn.click();
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return false;
|
|
199
|
-
})()
|
|
200
|
-
JS
|
|
201
|
-
|
|
202
|
-
if legends_closed
|
|
203
|
-
log 'Closed legends dialog, clicking slot again...'
|
|
204
|
-
sleep 1
|
|
205
|
-
|
|
206
|
-
# Click the slot again to open booking dialog
|
|
207
|
-
click_result = @browser.evaluate(<<~JS)
|
|
208
|
-
(function() {
|
|
209
|
-
var div = document.querySelector('div[data-area-id="' + #{slot_info[:area_id]} + '"]');
|
|
210
|
-
if (!div) return { clicked: false, reason: 'slot not found' };
|
|
211
|
-
|
|
212
|
-
var td = div.parentElement;
|
|
213
|
-
while (td && td.tagName !== 'TD') {
|
|
214
|
-
td = td.parentElement;
|
|
215
|
-
}
|
|
216
|
-
if (!td) return { clicked: false, reason: 'td not found' };
|
|
217
|
-
|
|
218
|
-
// Click and immediately check for dialog or auto-booking
|
|
219
|
-
td.click();
|
|
220
|
-
|
|
221
|
-
// Wait just a tiny bit for potential dialog
|
|
222
|
-
var startTime = Date.now();
|
|
223
|
-
while (Date.now() - startTime < 500) {
|
|
224
|
-
// Busy wait for 500ms
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Check what happened
|
|
228
|
-
var dialogs = document.querySelectorAll('.ui-dialog');
|
|
229
|
-
var visibleDialogs = 0;
|
|
230
|
-
var saveButton = null;
|
|
231
|
-
|
|
232
|
-
for (var i = 0; i < dialogs.length; i++) {
|
|
233
|
-
var dialog = dialogs[i];
|
|
234
|
-
var style = window.getComputedStyle(dialog);
|
|
235
|
-
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
|
236
|
-
|
|
237
|
-
visibleDialogs++;
|
|
238
|
-
|
|
239
|
-
// Try to find and click save button immediately
|
|
240
|
-
var btn =
|
|
241
|
-
dialog.querySelector('a.btn-save') ||
|
|
242
|
-
dialog.querySelector('button.btn-save') ||
|
|
243
|
-
dialog.querySelector('a[id*="save"]') ||
|
|
244
|
-
dialog.querySelector('button[id*="save"]') ||
|
|
245
|
-
dialog.querySelector('a.ui-commandlink:not(.cross)') ||
|
|
246
|
-
dialog.querySelector('.ui-button:not(.ui-dialog-titlebar-close):not(.cross)') ||
|
|
247
|
-
dialog.querySelector('a.ui-area-btn-success') ||
|
|
248
|
-
dialog.querySelector('button[type="submit"]');
|
|
249
|
-
|
|
250
|
-
if (btn) {
|
|
251
|
-
btn.click();
|
|
252
|
-
saveButton = { id: btn.id, text: btn.textContent.trim() };
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
clicked: true,
|
|
259
|
-
visibleDialogs: visibleDialogs,
|
|
260
|
-
saveButton: saveButton,
|
|
261
|
-
saveClicked: !!saveButton
|
|
262
|
-
};
|
|
263
|
-
})()
|
|
264
|
-
JS
|
|
265
|
-
|
|
266
|
-
log "Second click result: #{click_result.inspect}"
|
|
267
|
-
|
|
268
|
-
if click_result['saveClicked']
|
|
269
|
-
log "Save button clicked immediately after dialog opened!"
|
|
270
|
-
sleep 3
|
|
271
|
-
page_text = @browser.evaluate('document.body.textContent')
|
|
272
|
-
if page_text =~ /confirmed|success|booked|reservation.*created/i
|
|
273
|
-
log 'Booking appears confirmed based on page content'
|
|
274
|
-
return {
|
|
275
|
-
success: true,
|
|
276
|
-
court: slot_info[:court],
|
|
277
|
-
time: "#{slot_info[:time]} - #{(Time.parse(slot_info[:time]) + 3600).strftime('%-I:%M %p').upcase}",
|
|
278
|
-
confirmation: 'Booking confirmed'
|
|
279
|
-
}
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
# In debug mode, wait so user can see the browser
|
|
284
|
-
if @debug
|
|
285
|
-
log 'DEBUG: Waiting 10 seconds so you can see the browser state...'
|
|
286
|
-
sleep 10
|
|
287
|
-
else
|
|
288
|
-
sleep 2
|
|
289
|
-
end
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
# Check if booking dialog opened
|
|
293
|
-
dialog_visible = @browser.evaluate(<<~JS)
|
|
130
|
+
# Check if reservation panel appeared with Save button
|
|
131
|
+
panel_state = @browser.evaluate(<<~JS)
|
|
294
132
|
(function() {
|
|
295
|
-
var
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
133
|
+
var panel = document.querySelector('[id*="reservationPanel"]');
|
|
134
|
+
var saveBtn = document.querySelector('.btn-save');
|
|
135
|
+
var fromTime = document.querySelector('select[name*="fromTime_input"]');
|
|
136
|
+
return {
|
|
137
|
+
hasPanel: !!panel,
|
|
138
|
+
panelVisible: panel ? panel.offsetWidth > 0 : false,
|
|
139
|
+
hasSaveBtn: !!saveBtn,
|
|
140
|
+
saveBtnVisible: saveBtn ? saveBtn.offsetWidth > 0 : false,
|
|
141
|
+
saveBtnId: saveBtn ? saveBtn.id : null,
|
|
142
|
+
fromTime: fromTime ? fromTime.value : null
|
|
143
|
+
};
|
|
304
144
|
})()
|
|
305
145
|
JS
|
|
146
|
+
log "Reservation panel: #{panel_state.inspect}"
|
|
306
147
|
|
|
307
|
-
unless
|
|
308
|
-
log '
|
|
148
|
+
unless panel_state['saveBtnVisible']
|
|
149
|
+
log 'Save button not visible after slot click'
|
|
309
150
|
if @debug
|
|
310
|
-
@browser.screenshot(path: '/tmp/
|
|
311
|
-
log 'Saved screenshot to /tmp/
|
|
151
|
+
@browser.screenshot(path: '/tmp/no_save_btn.png', full: true)
|
|
152
|
+
log 'Saved screenshot to /tmp/no_save_btn.png'
|
|
312
153
|
end
|
|
313
|
-
raise BookingError, '
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
log 'Booking dialog opened'
|
|
317
|
-
|
|
318
|
-
# Capture dialog HTML for debugging
|
|
319
|
-
if @debug
|
|
320
|
-
dialog_html = @browser.evaluate(<<~JS)
|
|
321
|
-
(function() {
|
|
322
|
-
var dialogs = document.querySelectorAll('.ui-dialog');
|
|
323
|
-
for (var i = 0; i < dialogs.length; i++) {
|
|
324
|
-
var dialog = dialogs[i];
|
|
325
|
-
var style = window.getComputedStyle(dialog);
|
|
326
|
-
if (style.display !== 'none' && style.visibility !== 'hidden') {
|
|
327
|
-
return dialog.outerHTML;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return null;
|
|
331
|
-
})()
|
|
332
|
-
JS
|
|
333
|
-
|
|
334
|
-
if dialog_html
|
|
335
|
-
File.write('/tmp/booking_dialog.html', dialog_html)
|
|
336
|
-
log 'Saved booking dialog HTML to /tmp/booking_dialog.html'
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
@browser.screenshot(path: '/tmp/booking_dialog.png')
|
|
340
|
-
log 'Saved screenshot to /tmp/booking_dialog.png'
|
|
154
|
+
raise BookingError, 'Reservation panel did not appear after clicking slot'
|
|
341
155
|
end
|
|
342
156
|
|
|
343
157
|
if dry_run
|
|
344
|
-
log 'Dry run -
|
|
345
|
-
close_dialog
|
|
158
|
+
log 'Dry run - not clicking Save'
|
|
346
159
|
return {
|
|
347
160
|
success: true,
|
|
348
161
|
dry_run: true,
|
|
349
162
|
court: slot_info[:court],
|
|
350
|
-
time: time,
|
|
351
|
-
message:
|
|
163
|
+
time: slot_info[:time],
|
|
164
|
+
message: "Dry run - reservation panel opened for #{slot_info[:court]} at #{slot_info[:time]}"
|
|
352
165
|
}
|
|
353
166
|
end
|
|
354
167
|
|
|
355
|
-
#
|
|
356
|
-
log '
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
# Check for save button immediately
|
|
360
|
-
quick_check = @browser.evaluate(<<~JS)
|
|
168
|
+
# Click the Save button
|
|
169
|
+
log 'Clicking Save button...'
|
|
170
|
+
save_btn_id = panel_state['saveBtnId']
|
|
171
|
+
@browser.evaluate(<<~JS)
|
|
361
172
|
(function() {
|
|
362
|
-
var
|
|
363
|
-
|
|
364
|
-
var saveButton = null;
|
|
365
|
-
|
|
366
|
-
for (var i = 0; i < dialogs.length; i++) {
|
|
367
|
-
var dialog = dialogs[i];
|
|
368
|
-
var style = window.getComputedStyle(dialog);
|
|
369
|
-
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
|
370
|
-
|
|
371
|
-
visibleCount++;
|
|
372
|
-
|
|
373
|
-
// Try to find save button
|
|
374
|
-
var btn =
|
|
375
|
-
dialog.querySelector('a.btn-save') ||
|
|
376
|
-
dialog.querySelector('button.btn-save') ||
|
|
377
|
-
dialog.querySelector('a[id*="save"]') ||
|
|
378
|
-
dialog.querySelector('button[id*="save"]') ||
|
|
379
|
-
dialog.querySelector('a.ui-commandlink:not(.cross)') ||
|
|
380
|
-
dialog.querySelector('.ui-button:not(.ui-dialog-titlebar-close):not(.cross)') ||
|
|
381
|
-
dialog.querySelector('a.ui-area-btn-success') ||
|
|
382
|
-
dialog.querySelector('button[type="submit"]');
|
|
383
|
-
|
|
384
|
-
if (btn) {
|
|
385
|
-
saveButton = {
|
|
386
|
-
id: btn.id || '',
|
|
387
|
-
text: btn.textContent.trim(),
|
|
388
|
-
className: btn.className || ''
|
|
389
|
-
};
|
|
390
|
-
btn.click();
|
|
391
|
-
return { found: true, clicked: true, button: saveButton, visibleCount: visibleCount };
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return { found: false, clicked: false, visibleCount: visibleCount };
|
|
173
|
+
var btn = document.getElementById('#{save_btn_id}');
|
|
174
|
+
if (btn) btn.click();
|
|
396
175
|
})()
|
|
397
176
|
JS
|
|
398
177
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
log "Save button clicked immediately!"
|
|
403
|
-
sleep 3
|
|
404
|
-
page_text = @browser.evaluate('document.body.textContent')
|
|
405
|
-
if page_text =~ /confirmed|success|booked/i
|
|
406
|
-
log 'Booking confirmed'
|
|
407
|
-
return {
|
|
408
|
-
success: true,
|
|
409
|
-
court: slot_info[:court],
|
|
410
|
-
time: "#{slot_info[:time]} - #{(Time.parse(slot_info[:time]) + 3600).strftime('%-I:%M %p').upcase}",
|
|
411
|
-
confirmation: 'Booking confirmed'
|
|
412
|
-
}
|
|
413
|
-
end
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
sleep 2 # Wait a bit more
|
|
178
|
+
# Wait for save AJAX to complete
|
|
179
|
+
wait_for_ajax(timeout: 10)
|
|
180
|
+
sleep 2
|
|
417
181
|
|
|
418
|
-
#
|
|
419
|
-
|
|
182
|
+
# Check for success or error
|
|
183
|
+
result_state = @browser.evaluate(<<~JS)
|
|
420
184
|
(function() {
|
|
421
|
-
var
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
var
|
|
428
|
-
if (
|
|
429
|
-
visibleDialogs.push(dialog);
|
|
430
|
-
}
|
|
185
|
+
var body = document.body.textContent;
|
|
186
|
+
|
|
187
|
+
// Check for PrimeFaces error messages
|
|
188
|
+
var msgs = document.querySelectorAll('.ui-messages-error, .ui-message-error');
|
|
189
|
+
var errors = [];
|
|
190
|
+
for (var i = 0; i < msgs.length; i++) {
|
|
191
|
+
var txt = msgs[i].textContent.trim();
|
|
192
|
+
if (txt) errors.push(txt);
|
|
431
193
|
}
|
|
432
194
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
var
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
var btn = allBtns[j];
|
|
443
|
-
buttons.push({
|
|
444
|
-
tag: btn.tagName,
|
|
445
|
-
id: btn.id || '',
|
|
446
|
-
className: btn.className || '',
|
|
447
|
-
text: btn.textContent.trim(),
|
|
448
|
-
disabled: btn.disabled || btn.className.indexOf('disabled') > -1
|
|
449
|
-
});
|
|
195
|
+
// Check PrimeFaces Growl notifications (success/error popups)
|
|
196
|
+
var growlMsgs = document.querySelectorAll('.ui-growl-message, .ui-growl-item');
|
|
197
|
+
var growls = [];
|
|
198
|
+
for (var i = 0; i < growlMsgs.length; i++) {
|
|
199
|
+
var summary = growlMsgs[i].querySelector('.ui-growl-title');
|
|
200
|
+
var detail = growlMsgs[i].querySelector('.ui-growl-message');
|
|
201
|
+
var text = (summary ? summary.textContent : '') + ' ' + (detail ? detail.textContent : '');
|
|
202
|
+
if (!text.trim()) text = growlMsgs[i].textContent.trim();
|
|
203
|
+
if (text.trim()) growls.push(text.trim());
|
|
450
204
|
}
|
|
451
205
|
|
|
452
|
-
|
|
206
|
+
// Check if reservation panel is gone (success) or still showing (error)
|
|
207
|
+
var panel = document.querySelector('[id*="reservationPanel"]');
|
|
208
|
+
var panelVisible = panel ? panel.offsetWidth > 0 : false;
|
|
209
|
+
var saveBtn = document.querySelector('.btn-save');
|
|
210
|
+
var saveBtnVisible = saveBtn ? saveBtn.offsetWidth > 0 : false;
|
|
211
|
+
|
|
212
|
+
// Check for Back button (visible means we're still on the reservation form)
|
|
213
|
+
var backBtn = document.querySelector('.btn-back');
|
|
214
|
+
var backBtnVisible = backBtn ? backBtn.offsetWidth > 0 : false;
|
|
215
|
+
|
|
216
|
+
var bodyLower = body.toLowerCase();
|
|
453
217
|
return {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
218
|
+
errors: errors,
|
|
219
|
+
growls: growls,
|
|
220
|
+
panelStillVisible: panelVisible,
|
|
221
|
+
saveBtnStillVisible: saveBtnVisible,
|
|
222
|
+
backBtnStillVisible: backBtnVisible,
|
|
223
|
+
hasConfirmation: bodyLower.indexOf('confirmed') > -1 ||
|
|
224
|
+
bodyLower.indexOf('success') > -1 ||
|
|
225
|
+
bodyLower.indexOf('booked') > -1 ||
|
|
226
|
+
bodyLower.indexOf('reservation has been') > -1
|
|
459
227
|
};
|
|
460
228
|
})()
|
|
461
229
|
JS
|
|
230
|
+
log "After save: #{result_state.inspect}"
|
|
462
231
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
(function() {
|
|
468
|
-
// Find all visible dialogs
|
|
469
|
-
var dialogs = document.querySelectorAll('.ui-dialog');
|
|
470
|
-
var visibleDialogs = [];
|
|
471
|
-
for (var i = 0; i < dialogs.length; i++) {
|
|
472
|
-
var dialog = dialogs[i];
|
|
473
|
-
var style = window.getComputedStyle(dialog);
|
|
474
|
-
if (style.display !== 'none' && style.visibility !== 'hidden') {
|
|
475
|
-
visibleDialogs.push(dialog);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
232
|
+
if @debug
|
|
233
|
+
@browser.screenshot(path: '/tmp/after_save.png', full: true)
|
|
234
|
+
log 'Saved screenshot to /tmp/after_save.png'
|
|
235
|
+
end
|
|
478
236
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
var targetDialog = visibleDialogs[visibleDialogs.length - 1];
|
|
483
|
-
|
|
484
|
-
// Try to find save/submit button in this dialog
|
|
485
|
-
var saveBtn =
|
|
486
|
-
targetDialog.querySelector('a.btn-save') ||
|
|
487
|
-
targetDialog.querySelector('button.btn-save') ||
|
|
488
|
-
targetDialog.querySelector('a[id*="save"]') ||
|
|
489
|
-
targetDialog.querySelector('button[id*="save"]') ||
|
|
490
|
-
targetDialog.querySelector('a.ui-commandlink:not(.cross)') ||
|
|
491
|
-
targetDialog.querySelector('.ui-button:not(.ui-dialog-titlebar-close):not(.cross)') ||
|
|
492
|
-
targetDialog.querySelector('a.ui-area-btn-success') ||
|
|
493
|
-
targetDialog.querySelector('button[type="submit"]');
|
|
494
|
-
|
|
495
|
-
if (saveBtn) {
|
|
496
|
-
var btnInfo = {
|
|
497
|
-
id: saveBtn.id || '',
|
|
498
|
-
className: saveBtn.className || '',
|
|
499
|
-
text: saveBtn.textContent.trim(),
|
|
500
|
-
disabled: saveBtn.disabled || saveBtn.className.indexOf('disabled') > -1
|
|
501
|
-
};
|
|
502
|
-
console.log('Found save button:', saveBtn.id || saveBtn.className);
|
|
503
|
-
saveBtn.click();
|
|
504
|
-
return { clicked: true, button: btnInfo };
|
|
505
|
-
}
|
|
506
|
-
return { clicked: false, reason: 'no save button found', dialogCount: visibleDialogs.length };
|
|
507
|
-
})()
|
|
508
|
-
JS
|
|
237
|
+
if result_state['errors'].any?
|
|
238
|
+
raise BookingError, "Booking failed: #{result_state['errors'].join(', ')}"
|
|
239
|
+
end
|
|
509
240
|
|
|
510
|
-
|
|
511
|
-
|
|
241
|
+
# Check growl messages for errors
|
|
242
|
+
growl_errors = result_state['growls'].select { |g| g =~ /error|fail|unable|invalid/i }
|
|
243
|
+
if growl_errors.any?
|
|
244
|
+
raise BookingError, "Booking failed: #{growl_errors.join(', ')}"
|
|
245
|
+
end
|
|
512
246
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
247
|
+
# Success indicators:
|
|
248
|
+
# 1. Save button disappeared (panel closed after successful save)
|
|
249
|
+
# 2. Confirmation text in body or growl
|
|
250
|
+
# 3. Back button gone (returned to slot view)
|
|
251
|
+
growl_success = result_state['growls'].any? { |g| g =~ /success|confirm|booked|reserved/i }
|
|
516
252
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
else
|
|
528
|
-
log 'Warning: Save button clicked but no confirmation message detected'
|
|
529
|
-
return {
|
|
530
|
-
success: false,
|
|
531
|
-
error: 'Booking uncertain - please check My Reservations'
|
|
532
|
-
}
|
|
533
|
-
end
|
|
253
|
+
if !result_state['saveBtnStillVisible'] || result_state['hasConfirmation'] || growl_success
|
|
254
|
+
confirmation_msg = result_state['growls'].first || 'Booking confirmed'
|
|
255
|
+
log "Booking confirmed: #{confirmation_msg}"
|
|
256
|
+
{
|
|
257
|
+
success: true,
|
|
258
|
+
court: slot_info[:court],
|
|
259
|
+
time: slot_info[:time],
|
|
260
|
+
confirmation: confirmation_msg
|
|
261
|
+
}
|
|
534
262
|
else
|
|
535
|
-
log '
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
# Transfer cookies from browser to HTTP client
|
|
541
|
-
browser_cookies = @browser.cookies.all
|
|
542
|
-
browser_cookies.each do |name, cookie|
|
|
543
|
-
http_client.cookies[name] = cookie.value
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
# Calculate end time (assuming 1-hour slots)
|
|
547
|
-
start_time_obj = Time.parse(slot_info[:time])
|
|
548
|
-
end_time = (start_time_obj + 3600).strftime('%-I:%M %p').upcase
|
|
549
|
-
|
|
550
|
-
# Pass the slot info directly to avoid re-finding
|
|
551
|
-
result = http_client.book(
|
|
552
|
-
time,
|
|
553
|
-
date: date,
|
|
554
|
-
court: court,
|
|
555
|
-
activity: activity,
|
|
556
|
-
dry_run: false,
|
|
557
|
-
slot_info: {
|
|
558
|
-
area_id: slot_info[:area_id],
|
|
559
|
-
court: slot_info[:court],
|
|
560
|
-
start_time: slot_info[:time],
|
|
561
|
-
end_time: end_time
|
|
562
|
-
}
|
|
563
|
-
)
|
|
564
|
-
|
|
565
|
-
result
|
|
263
|
+
log 'Booking status uncertain - check My Reservations'
|
|
264
|
+
{
|
|
265
|
+
success: false,
|
|
266
|
+
error: 'Booking uncertain - please check My Reservations'
|
|
267
|
+
}
|
|
566
268
|
end
|
|
567
269
|
ensure
|
|
568
270
|
quit
|
|
@@ -646,46 +348,173 @@ module Truenorth
|
|
|
646
348
|
@browser.go_to("#{@base_url}/group/pages/facility-booking")
|
|
647
349
|
sleep 3
|
|
648
350
|
|
|
649
|
-
# Select activity
|
|
650
351
|
activity_id = Client::ACTIVITIES[activity.to_s.downcase] || '5'
|
|
651
|
-
select_activity(activity_id)
|
|
652
|
-
sleep 5
|
|
653
352
|
|
|
654
|
-
#
|
|
655
|
-
|
|
656
|
-
|
|
353
|
+
# Step 1: Change activity via PrimeFaces SelectOneMenu widget (triggers AJAX)
|
|
354
|
+
# This updates the server-side activity. Do NOT reload - that resets to default (golf).
|
|
355
|
+
select_activity_ui(activity_id)
|
|
356
|
+
wait_for_ajax
|
|
357
|
+
|
|
358
|
+
# Step 2: Navigate to correct date - the dateSelect AJAX renders the table
|
|
359
|
+
# with the correct activity from the session updated in step 1
|
|
360
|
+
navigate_to_date(date, force: true)
|
|
361
|
+
wait_for_ajax
|
|
362
|
+
|
|
363
|
+
# No extra sheetDate fix needed - the Calendar widget properly updates the server
|
|
364
|
+
|
|
365
|
+
# Verify correct activity courts are shown
|
|
366
|
+
verify = @browser.evaluate(<<~JS)
|
|
367
|
+
(function() {
|
|
368
|
+
var slots = document.querySelectorAll('td.slot div[data-area-id]');
|
|
369
|
+
var areaIds = {};
|
|
370
|
+
for (var i = 0; i < slots.length; i++) {
|
|
371
|
+
var aid = slots[i].getAttribute('data-area-id');
|
|
372
|
+
areaIds[aid] = (areaIds[aid] || 0) + 1;
|
|
373
|
+
}
|
|
374
|
+
return { totalSlots: slots.length, areaIds: areaIds };
|
|
375
|
+
})()
|
|
376
|
+
JS
|
|
377
|
+
log "After navigation - slots: #{verify['totalSlots']}, areas: #{verify['areaIds']}"
|
|
378
|
+
|
|
657
379
|
log "Navigated to #{activity} for #{date}"
|
|
658
|
-
log "Current URL: #{current_url}"
|
|
659
|
-
log "Page title: #{page_title}"
|
|
660
380
|
end
|
|
661
381
|
|
|
662
|
-
def
|
|
382
|
+
def wait_for_ajax(timeout: 15)
|
|
383
|
+
timeout.times do |i|
|
|
384
|
+
sleep 1
|
|
385
|
+
idle = @browser.evaluate(<<~JS)
|
|
386
|
+
(function() {
|
|
387
|
+
try {
|
|
388
|
+
if (typeof PrimeFaces !== 'undefined' && PrimeFaces.ajax) {
|
|
389
|
+
if (PrimeFaces.ajax.Queue && PrimeFaces.ajax.Queue.isEmpty) {
|
|
390
|
+
return PrimeFaces.ajax.Queue.isEmpty();
|
|
391
|
+
}
|
|
392
|
+
// Older PrimeFaces versions
|
|
393
|
+
if (PrimeFaces.ajax.QUEUE && PrimeFaces.ajax.QUEUE.isEmpty) {
|
|
394
|
+
return PrimeFaces.ajax.QUEUE.isEmpty();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return true;
|
|
398
|
+
} catch(e) { return true; }
|
|
399
|
+
})()
|
|
400
|
+
JS
|
|
401
|
+
if idle
|
|
402
|
+
log "AJAX completed after #{i + 1} seconds" if @debug
|
|
403
|
+
break
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
sleep 1 # Extra buffer for DOM rendering
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def select_activity_ui(activity_id)
|
|
663
410
|
log "Selecting activity ID: #{activity_id}"
|
|
664
|
-
|
|
411
|
+
|
|
412
|
+
# Use PrimeFaces SelectOneMenu widget's selectItem method (non-silent)
|
|
413
|
+
# This fires the change behavior AJAX, properly updating the server session
|
|
414
|
+
result = @browser.evaluate(<<~JS)
|
|
415
|
+
(function() {
|
|
416
|
+
for (var key in PrimeFaces.widgets) {
|
|
417
|
+
var w = PrimeFaces.widgets[key];
|
|
418
|
+
if (!w.input || !w.input[0] || w.input[0].tagName !== 'SELECT') continue;
|
|
419
|
+
var sel = w.input[0];
|
|
420
|
+
|
|
421
|
+
var targetIdx = -1;
|
|
422
|
+
for (var i = 0; i < sel.options.length; i++) {
|
|
423
|
+
if (sel.options[i].value === '#{activity_id}') {
|
|
424
|
+
targetIdx = i;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (targetIdx === -1) continue;
|
|
429
|
+
|
|
430
|
+
var oldValue = sel.value;
|
|
431
|
+
if (oldValue === '#{activity_id}') {
|
|
432
|
+
return { success: true, method: 'already_selected', value: oldValue };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// selectItem without silent flag triggers the change behavior AJAX
|
|
436
|
+
w.selectItem(w.items.eq(targetIdx));
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
success: true, method: 'ui_click',
|
|
440
|
+
oldValue: oldValue, newValue: sel.value,
|
|
441
|
+
targetIdx: targetIdx, widgetKey: key
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return { error: 'widget not found' };
|
|
445
|
+
})()
|
|
446
|
+
JS
|
|
447
|
+
log "Activity select result: #{result.inspect}"
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def navigate_to_date(date, force: false)
|
|
451
|
+
target_str = date.strftime('%m/%d/%Y')
|
|
452
|
+
|
|
453
|
+
current_date = @browser.evaluate(<<~JS)
|
|
454
|
+
(function() {
|
|
455
|
+
var input = document.querySelector('input[name*="sheetDate"]');
|
|
456
|
+
return input ? input.value : null;
|
|
457
|
+
})()
|
|
458
|
+
JS
|
|
459
|
+
|
|
460
|
+
if current_date == target_str && !force
|
|
461
|
+
log "Already on correct date: #{target_str}"
|
|
462
|
+
return
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
log "Navigating from #{current_date} to #{target_str}"
|
|
466
|
+
|
|
467
|
+
# Use the Calendar widget (j_idt79) which has setDate + dateSelect behavior.
|
|
468
|
+
# This properly updates the server's session date, unlike directly setting
|
|
469
|
+
# the sheetDate input which only updates the display.
|
|
470
|
+
result = @browser.evaluate(<<~JS)
|
|
665
471
|
(function() {
|
|
666
|
-
var
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
472
|
+
var parts = '#{target_str}'.split('/');
|
|
473
|
+
var targetDate = new Date(parseInt(parts[2]), parseInt(parts[0]) - 1, parseInt(parts[1]));
|
|
474
|
+
|
|
475
|
+
// Find the Calendar widget with setDate + dateSelect behavior
|
|
476
|
+
for (var key in PrimeFaces.widgets) {
|
|
477
|
+
var w = PrimeFaces.widgets[key];
|
|
478
|
+
if (typeof w.setDate !== 'function') continue;
|
|
479
|
+
if (!w.cfg || !w.cfg.behaviors || !w.cfg.behaviors.dateSelect) continue;
|
|
480
|
+
|
|
481
|
+
w.setDate(targetDate);
|
|
482
|
+
w.cfg.behaviors.dateSelect.call(w, {
|
|
483
|
+
params: [{ name: w.id + '_selectedDate', value: targetDate.getTime() }]
|
|
675
484
|
});
|
|
485
|
+
return { success: true, method: 'setDate+behavior', widgetKey: key };
|
|
676
486
|
}
|
|
487
|
+
|
|
488
|
+
// Fallback: click the day tab link if target is within the visible week
|
|
489
|
+
var dayLinks = document.querySelectorAll('a[id*="j_idt98"]');
|
|
490
|
+
for (var i = 0; i < dayLinks.length; i++) {
|
|
491
|
+
var dayNum = dayLinks[i].textContent.match(/\\b(\\d{1,2})\\b/);
|
|
492
|
+
if (dayNum && parseInt(dayNum[1]) === parseInt(parts[1])) {
|
|
493
|
+
dayLinks[i].click();
|
|
494
|
+
return { success: true, method: 'dayTab_click', day: dayNum[1] };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return { error: 'No Calendar widget or matching day tab found' };
|
|
677
499
|
})()
|
|
678
500
|
JS
|
|
501
|
+
log "Date navigation result: #{result.inspect}"
|
|
679
502
|
end
|
|
680
503
|
|
|
681
|
-
def find_and_click_slot(target_time, preferred_court)
|
|
504
|
+
def find_and_click_slot(target_time, preferred_court, booking_date = Date.today)
|
|
682
505
|
# Normalize time
|
|
683
506
|
normalized_time = target_time.strip.gsub(/^0/, '').upcase
|
|
684
507
|
|
|
685
|
-
#
|
|
508
|
+
# Only filter past-time slots for today/past dates.
|
|
509
|
+
# The page erroneously applies past-time class to future dates based on current time of day,
|
|
510
|
+
# but the server accepts bookings for those slots fine.
|
|
511
|
+
is_today = booking_date <= Date.today
|
|
512
|
+
slot_selector = is_today ? 'td.slot.open:not(.past-time)' : 'td.slot.open'
|
|
513
|
+
|
|
514
|
+
# Find all open slots
|
|
686
515
|
slots = @browser.evaluate(<<~JS)
|
|
687
516
|
(function() {
|
|
688
|
-
var openSlots = document.querySelectorAll('
|
|
517
|
+
var openSlots = document.querySelectorAll('#{slot_selector}');
|
|
689
518
|
var result = [];
|
|
690
519
|
for (var i = 0; i < openSlots.length; i++) {
|
|
691
520
|
var td = openSlots[i];
|
|
@@ -694,10 +523,12 @@ module Truenorth
|
|
|
694
523
|
var div = td.querySelector('div[data-start-time]');
|
|
695
524
|
var areaId = div ? div.getAttribute('data-area-id') : null;
|
|
696
525
|
|
|
697
|
-
if (
|
|
526
|
+
if (areaId) {
|
|
698
527
|
result.push({
|
|
699
528
|
time: time,
|
|
700
|
-
areaId: areaId
|
|
529
|
+
areaId: areaId,
|
|
530
|
+
startTime: div ? div.getAttribute('data-start-time') : '',
|
|
531
|
+
endTime: div ? div.getAttribute('data-end-time') : ''
|
|
701
532
|
});
|
|
702
533
|
}
|
|
703
534
|
}
|
|
@@ -705,9 +536,15 @@ module Truenorth
|
|
|
705
536
|
})()
|
|
706
537
|
JS
|
|
707
538
|
|
|
708
|
-
#
|
|
539
|
+
# Debug: show available times
|
|
540
|
+
unique_times = slots.map { |s| s['startTime'] }.uniq.sort
|
|
541
|
+
log "Available times (#{slots.length} bookable slots): #{unique_times.first(10).join(', ')}#{unique_times.length > 10 ? '...' : ''}"
|
|
542
|
+
log "Looking for: '#{normalized_time}'"
|
|
543
|
+
|
|
544
|
+
# Find matching slot by time display or data-start-time
|
|
709
545
|
matching_slot = slots.find do |slot|
|
|
710
|
-
time_match = slot['time'].gsub(/^0/, '').upcase == normalized_time
|
|
546
|
+
time_match = slot['time'].gsub(/^0/, '').upcase == normalized_time ||
|
|
547
|
+
slot['startTime'].gsub(/^0/, '').upcase == normalized_time
|
|
711
548
|
court_match = if preferred_court
|
|
712
549
|
court_name = Client::COURTS[slot['areaId']]
|
|
713
550
|
court_name&.downcase&.include?(preferred_court.downcase)
|
|
@@ -719,73 +556,34 @@ module Truenorth
|
|
|
719
556
|
|
|
720
557
|
return nil unless matching_slot
|
|
721
558
|
|
|
722
|
-
# Click the slot
|
|
723
559
|
court_name = Client::COURTS[matching_slot['areaId']]
|
|
724
|
-
log "Clicking slot: #{court_name} at #{matching_slot['
|
|
560
|
+
log "Clicking slot: #{court_name} at #{matching_slot['startTime']}"
|
|
725
561
|
|
|
726
|
-
#
|
|
727
|
-
|
|
562
|
+
# Call rc_showReservationScreen directly with slot parameters.
|
|
563
|
+
# This bypasses the client-side past-time check (which erroneously blocks
|
|
564
|
+
# future-date slots) and fires the PrimeFaces AJAX to open the reservation panel.
|
|
565
|
+
click_result = @browser.evaluate(<<~JS)
|
|
728
566
|
(function() {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
if (td) td.click();
|
|
736
|
-
}
|
|
567
|
+
rc_showReservationScreen([
|
|
568
|
+
{name: 'activityAreaId', value: '#{matching_slot['areaId']}'},
|
|
569
|
+
{name: 'startTime', value: '#{matching_slot['startTime']}'},
|
|
570
|
+
{name: 'endTime', value: '#{matching_slot['endTime']}'}
|
|
571
|
+
]);
|
|
572
|
+
return { success: true, areaId: '#{matching_slot['areaId']}', startTime: '#{matching_slot['startTime']}' };
|
|
737
573
|
})()
|
|
738
574
|
JS
|
|
575
|
+
log "Slot click result: #{click_result.inspect}"
|
|
576
|
+
|
|
577
|
+
wait_for_ajax
|
|
578
|
+
sleep 1
|
|
739
579
|
|
|
740
580
|
{
|
|
741
581
|
court: court_name,
|
|
742
|
-
time: matching_slot['
|
|
582
|
+
time: matching_slot['startTime'],
|
|
743
583
|
area_id: matching_slot['areaId']
|
|
744
584
|
}
|
|
745
585
|
end
|
|
746
586
|
|
|
747
|
-
def close_dialog
|
|
748
|
-
@browser.execute(<<~JS)
|
|
749
|
-
(function() {
|
|
750
|
-
var closeBtn = document.querySelector('.ui-dialog-titlebar-close');
|
|
751
|
-
if (closeBtn) closeBtn.click();
|
|
752
|
-
})()
|
|
753
|
-
JS
|
|
754
|
-
end
|
|
755
|
-
|
|
756
|
-
def submit_booking
|
|
757
|
-
log 'Submitting booking...'
|
|
758
|
-
|
|
759
|
-
# Try to find and trigger the save button via PrimeFaces
|
|
760
|
-
success = @browser.execute(<<~JS)
|
|
761
|
-
(function() {
|
|
762
|
-
// Look for the save button anywhere on the page
|
|
763
|
-
var saveBtn = document.querySelector('a.btn-save, button.btn-save') ||
|
|
764
|
-
document.querySelector('a[id*="save"]') ||
|
|
765
|
-
document.querySelector('button[id*="save"]') ||
|
|
766
|
-
document.querySelector('.ui-dialog a.ui-commandlink') ||
|
|
767
|
-
document.querySelector('.ui-dialog button[type="button"]');
|
|
768
|
-
|
|
769
|
-
if (saveBtn) {
|
|
770
|
-
console.log('Clicking save button:', saveBtn.id);
|
|
771
|
-
saveBtn.click();
|
|
772
|
-
return true;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// If no button found, try submitting via form
|
|
776
|
-
var form = document.querySelector('form[id*="activityForm"]');
|
|
777
|
-
if (form) {
|
|
778
|
-
console.log('Submitting form');
|
|
779
|
-
form.submit();
|
|
780
|
-
return true;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
return false;
|
|
784
|
-
})()
|
|
785
|
-
JS
|
|
786
|
-
|
|
787
|
-
log "Booking submission triggered: #{success}"
|
|
788
|
-
end
|
|
789
587
|
|
|
790
588
|
def parse_reservations_table
|
|
791
589
|
reservations = []
|
data/lib/truenorth/cli.rb
CHANGED
|
@@ -117,9 +117,10 @@ module Truenorth
|
|
|
117
117
|
rescue Error => e
|
|
118
118
|
say "Error: #{e.message}", :red
|
|
119
119
|
|
|
120
|
-
# If no slot available, show nearby available times
|
|
120
|
+
# If no slot available, show nearby available times using HTTP client
|
|
121
121
|
if e.message.include?('No slot available')
|
|
122
|
-
|
|
122
|
+
http_client = Client.new(debug: options[:debug])
|
|
123
|
+
show_nearby_availability(http_client, date, time, activity)
|
|
123
124
|
end
|
|
124
125
|
|
|
125
126
|
exit 1
|
data/lib/truenorth/client.rb
CHANGED
|
@@ -28,23 +28,22 @@ module Truenorth
|
|
|
28
28
|
'meeting' => '8'
|
|
29
29
|
}.freeze
|
|
30
30
|
|
|
31
|
-
# Court area IDs
|
|
31
|
+
# Court area IDs (from page column headers)
|
|
32
32
|
COURTS = {
|
|
33
|
-
'16' => '
|
|
34
|
-
'17' => '
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'32' => 'Court 3'
|
|
33
|
+
'16' => 'Golf Simulator 1',
|
|
34
|
+
'17' => 'Golf Simulator 2',
|
|
35
|
+
'30' => 'Squash Court 1',
|
|
36
|
+
'31' => 'Squash Court 2',
|
|
37
|
+
'32' => 'Squash Court 3'
|
|
39
38
|
}.freeze
|
|
40
39
|
|
|
41
40
|
# Court IDs by activity
|
|
42
41
|
COURT_IDS_BY_ACTIVITY = {
|
|
43
|
-
'squash' => %w[
|
|
44
|
-
'golf' => %w[
|
|
45
|
-
'music' => %w[16 17
|
|
46
|
-
'room' => %w[30 31 32],
|
|
47
|
-
'meeting' => %w[30 31 32]
|
|
42
|
+
'squash' => %w[30 31 32],
|
|
43
|
+
'golf' => %w[16 17],
|
|
44
|
+
'music' => %w[16 17], # May need to update these
|
|
45
|
+
'room' => %w[30 31 32], # May need to update these
|
|
46
|
+
'meeting' => %w[30 31 32]
|
|
48
47
|
}.freeze
|
|
49
48
|
|
|
50
49
|
attr_reader :cookies, :debug_log, :logged_in
|
|
@@ -56,7 +55,8 @@ module Truenorth
|
|
|
56
55
|
@cookies = Config.cookies || {}
|
|
57
56
|
@debug = debug
|
|
58
57
|
@debug_log = StringIO.new
|
|
59
|
-
@logged_in = !@cookies.empty? # If we have cookies,
|
|
58
|
+
@logged_in = !@cookies.empty? # If we have cookies, will verify on first use
|
|
59
|
+
@last_verified_response = nil
|
|
60
60
|
|
|
61
61
|
log "Loaded #{@cookies.length} cookies from cache" if @logged_in && @debug
|
|
62
62
|
end
|
|
@@ -286,7 +286,9 @@ module Truenorth
|
|
|
286
286
|
|
|
287
287
|
log "\n=== GET RESERVATIONS ==="
|
|
288
288
|
|
|
289
|
-
response
|
|
289
|
+
# Reuse the response from session verification if available
|
|
290
|
+
response = @last_verified_response || get(RESERVATIONS_PATH)
|
|
291
|
+
@last_verified_response = nil
|
|
290
292
|
html = Nokogiri::HTML(response.body)
|
|
291
293
|
|
|
292
294
|
reservations = []
|
|
@@ -478,11 +480,31 @@ module Truenorth
|
|
|
478
480
|
private
|
|
479
481
|
|
|
480
482
|
def ensure_logged_in!
|
|
481
|
-
|
|
483
|
+
if @logged_in
|
|
484
|
+
# Verify cached session is still valid with a lightweight check
|
|
485
|
+
response = get(RESERVATIONS_PATH)
|
|
486
|
+
if authenticated_response?(response)
|
|
487
|
+
@last_verified_response = response
|
|
488
|
+
return
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Session expired - clear stale state and re-login
|
|
492
|
+
log 'Session expired, re-authenticating...'
|
|
493
|
+
@logged_in = false
|
|
494
|
+
@cookies = {}
|
|
495
|
+
end
|
|
482
496
|
|
|
483
497
|
login
|
|
484
498
|
end
|
|
485
499
|
|
|
500
|
+
# Check if a response is from an authenticated session (not a login page)
|
|
501
|
+
def authenticated_response?(response)
|
|
502
|
+
return false unless response.is_a?(Net::HTTPSuccess)
|
|
503
|
+
|
|
504
|
+
body = response.body
|
|
505
|
+
!body.include?('LoginPortlet') && body.include?('Sign Out')
|
|
506
|
+
end
|
|
507
|
+
|
|
486
508
|
def parse_slots(html)
|
|
487
509
|
slots = {}
|
|
488
510
|
|
data/lib/truenorth/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: truenorth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- usiegj00
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|