truenorth 0.2.6 → 0.2.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77aa3d893615956dc5dad5e8adc512bbdeb436aa99f6b04be100d246ca3d59f7
4
- data.tar.gz: '0009efe2c7af6f8425018ebcbcf44b6d7c0295c83f08e90b00368358edd39205'
3
+ metadata.gz: ebf93538d8d794249ddcb8f4ddb2832eba001e6758d275c4ea59af595777d50e
4
+ data.tar.gz: e857b71120bf6c327fb83284ad901472656e12f0df542abd75223a953c6ea202
5
5
  SHA512:
6
- metadata.gz: 53c557e2417576cfaf637ada5e8057e15ff6fda7db6cc898efb9b99d8620c0b09e15fd2dcf3a8dadb8c31fec3e038b0aa0c7db434bd7382532aea7970fba3f90
7
- data.tar.gz: acc45f0794fb39ec448ba78abb8834568b0b8a050a91fd371fd55f1967a161bd1b2884f371329db24f1220454ddc00d9ecc7074ba48e42e5eae728741d97fed0
6
+ metadata.gz: c4b4e536e26ee506d3ee57b1cc9f186f9696c2f10ae61ab834e4dd40999d36381f1aa01b03096c1eb118ab92c720bef71d1b74f07c18c37fd6050545d13b67ad
7
+ data.tar.gz: '0619e6eb3fcb0220aebdcc6bb13f60c452d157c6e772dbd63596981c0dc4b6fed5cdbd8acf16a3c00916b853791875020651a0a3c5fe326fa8115a93fae5ab76'
data/lib/truenorth/cli.rb CHANGED
@@ -97,6 +97,12 @@ module Truenorth
97
97
  end
98
98
  rescue Error => e
99
99
  say "Error: #{e.message}", :red
100
+
101
+ # If no slot available, show nearby available times
102
+ if e.message.include?('No slot available')
103
+ show_nearby_availability(client, date, time, activity)
104
+ end
105
+
100
106
  exit 1
101
107
  end
102
108
 
@@ -226,6 +232,56 @@ module Truenorth
226
232
 
227
233
  private
228
234
 
235
+ def show_nearby_availability(client, date, requested_time, activity)
236
+ say "\nChecking available times for #{date}...", :cyan
237
+
238
+ begin
239
+ result = client.availability(date, activity: activity)
240
+
241
+ if result[:slots].empty?
242
+ say "No #{activity} slots available on #{date}", :yellow
243
+ else
244
+ say "\nAvailable #{activity} times:", :green
245
+
246
+ # Parse requested time to find nearby slots
247
+ requested_hour = parse_time_to_minutes(requested_time)
248
+
249
+ # Sort slots by proximity to requested time
250
+ sorted_slots = result[:slots].sort_by do |time_str, _courts|
251
+ slot_minutes = parse_time_to_minutes(time_str)
252
+ (slot_minutes - requested_hour).abs
253
+ end
254
+
255
+ # Show up to 10 nearest slots
256
+ sorted_slots.first(10).each do |time_str, courts|
257
+ say " #{time_str}: #{courts.join(', ')}"
258
+ end
259
+
260
+ if result[:slots].length > 10
261
+ say "\n ... and #{result[:slots].length - 10} more times", :cyan
262
+ end
263
+ end
264
+ rescue Error => e
265
+ say "Could not fetch availability: #{e.message}", :yellow
266
+ end
267
+ end
268
+
269
+ def parse_time_to_minutes(time_str)
270
+ # Convert "10:00 AM" or "10am" to minutes since midnight
271
+ if time_str =~ /(\d{1,2}):?(\d{2})?\s*(am|pm)/i
272
+ hour = ::Regexp.last_match(1).to_i
273
+ minute = ::Regexp.last_match(2)&.to_i || 0
274
+ period = ::Regexp.last_match(3).upcase
275
+
276
+ hour = 0 if hour == 12 && period == 'AM'
277
+ hour += 12 if period == 'PM' && hour != 12
278
+
279
+ hour * 60 + minute
280
+ else
281
+ 0
282
+ end
283
+ end
284
+
229
285
  def display_reservations_table(results)
230
286
  # Get terminal width
231
287
  term_width = begin
@@ -5,6 +5,7 @@ require 'uri'
5
5
  require 'nokogiri'
6
6
  require 'json'
7
7
  require 'base64'
8
+ require 'date'
8
9
 
9
10
  module Truenorth
10
11
  # HTTP client for NorthStar facility booking systems
@@ -283,7 +284,8 @@ module Truenorth
283
284
 
284
285
  raise BookingError, 'Could not extract view state' unless view_state
285
286
 
286
- # Build the cancel AJAX request
287
+ # Step 1: Click the cancel button to open the confirmation dialog
288
+ # The dialog button will be enabled in the AJAX response
287
289
  ajax_url = "#{@base_url}#{RESERVATIONS_PATH}?p_p_id=memberReservations_WAR_northstarportlet" \
288
290
  '&p_p_lifecycle=2&p_p_state=normal&p_p_mode=view' \
289
291
  '&p_p_cacheability=cacheLevelPage' \
@@ -301,50 +303,98 @@ module Truenorth
301
303
 
302
304
  response = post_ajax(ajax_url, form_data)
303
305
 
304
- if response.is_a?(Net::HTTPSuccess)
305
- body = response.body
306
-
307
- # Log response for debugging
308
- log "Response length: #{body.length}"
309
- log "Response preview: #{body[0..500]}"
310
-
311
- # Check for specific PrimeFaces success indicators
312
- # Look for actual cancellation confirmation messages
313
- success_indicators = [
314
- /cancelled.*successfully/i,
315
- /reservation.*cancelled/i,
316
- /successfully.*cancelled/i,
317
- /<update.*id=".*growl"/i # PrimeFaces growl messages usually indicate success/error
318
- ]
319
-
320
- # Check for error indicators
321
- error_indicators = [
322
- /error/i,
323
- /failed/i,
324
- /unable/i,
325
- /cannot.*cancel/i
326
- ]
327
-
328
- has_error = error_indicators.any? { |pattern| body =~ pattern }
329
- has_success = success_indicators.any? { |pattern| body =~ pattern }
330
-
331
- if has_error
332
- # Extract error message if present
333
- error_msg = body[/error[^<]*|failed[^<]*/i] || 'Cancellation may have failed'
334
- log "Error detected: #{error_msg}"
335
- { success: false, error: error_msg }
336
- elsif has_success || body.length < 500
337
- # Very short response likely means success (no error message)
338
- log 'Cancellation appears successful'
306
+ unless response.is_a?(Net::HTTPSuccess)
307
+ return { success: false, error: "HTTP #{response.code}" }
308
+ end
309
+
310
+ body = response.body
311
+ log "Step 1 response length: #{body.length}"
312
+
313
+ # Check if a confirmation dialog was opened
314
+ if body.include?("PF('cancelReservationDialog').show()") || body.include?('.show()')
315
+ log "Confirmation dialog opened"
316
+
317
+ # Extract the updated ViewState from the AJAX response
318
+ new_view_state = extract_view_state_from_ajax(body) || view_state
319
+
320
+ # Parse the AJAX response to find the enabled YES button
321
+ response_html = Nokogiri::HTML(body)
322
+
323
+ # Look for the YES button in the dialog (not in the reservation list!)
324
+ # The YES button has specific characteristics:
325
+ # 1. Contains text "YES"
326
+ # 2. Has class "ui-area-btn-danger"
327
+ # 3. Is inside the cancel dialog (j_idt274)
328
+ confirm_button = response_html.css('div[id*="j_idt274"] a.ui-area-btn-danger').find do |link|
329
+ link.text.strip.upcase.include?('YES')
330
+ end
331
+
332
+ # Fallback: look for any YES button with the right classes
333
+ unless confirm_button
334
+ confirm_button = response_html.css('a.ui-commandlink').find do |link|
335
+ link_text = link.text.strip.upcase
336
+ link_class = link['class'].to_s
337
+ link_text == 'YES' && link_class.include?('ui-area-btn-danger') &&
338
+ !link_class.include?('disabled')
339
+ end
340
+ end
341
+
342
+ unless confirm_button
343
+ log "Could not find enabled YES button in response"
344
+ log "Response preview: #{body[0..2000]}"
345
+ return { success: false, error: 'Could not find confirmation button' }
346
+ end
347
+
348
+ confirm_button_id = confirm_button['id']
349
+ log "Found confirmation button in response: #{confirm_button_id}"
350
+
351
+ # Step 2: Click the "Yes" button to actually cancel
352
+ confirm_data = {
353
+ 'javax.faces.partial.ajax' => 'true',
354
+ 'javax.faces.source' => confirm_button_id,
355
+ 'javax.faces.partial.execute' => '@all',
356
+ 'javax.faces.partial.render' => form_id,
357
+ form_id => form_id,
358
+ 'javax.faces.ViewState' => new_view_state,
359
+ confirm_button_id => confirm_button_id
360
+ }
361
+
362
+ log "Clicking confirmation button: #{confirm_button_id}"
363
+ confirm_response = post_ajax(ajax_url, confirm_data)
364
+
365
+ if confirm_response.is_a?(Net::HTTPSuccess)
366
+ confirm_body = confirm_response.body
367
+ log "Step 2 response length: #{confirm_body.length}"
368
+ log "Step 2 response preview: #{confirm_body[0..500]}"
369
+
370
+ # Check for success indicators
371
+ if confirm_body =~ /cancelled.*successfully/i ||
372
+ confirm_body =~ /reservation.*cancelled/i ||
373
+ confirm_body =~ /successfully.*cancelled/i ||
374
+ confirm_body.include?('growl') ||
375
+ confirm_body.length < 1000
376
+ log 'Cancellation confirmed successfully'
377
+ { success: true, message: 'Reservation cancelled' }
378
+ else
379
+ log "Warning: Uncertain confirmation response"
380
+ { success: true, message: 'Cancellation likely succeeded (please verify)' }
381
+ end
382
+ else
383
+ { success: false, error: "Confirmation failed: HTTP #{confirm_response.code}" }
384
+ end
385
+ else
386
+ # No dialog - check if it was directly cancelled
387
+ log "No confirmation dialog detected"
388
+
389
+ if body =~ /cancelled.*successfully/i ||
390
+ body =~ /reservation.*cancelled/i ||
391
+ body.length < 500
392
+ log 'Direct cancellation successful'
339
393
  { success: true, message: 'Reservation cancelled' }
340
394
  else
341
- # Ambiguous - log full response for analysis
342
- log "Ambiguous response (#{body.length} bytes)"
343
- log "Full response: #{body}"
395
+ log "Uncertain result (#{body.length} bytes)"
344
396
  { success: false, error: 'Uncertain if cancellation succeeded - please verify' }
345
397
  end
346
- else
347
- { success: false, error: "HTTP #{response.code}" }
348
398
  end
349
399
  end
350
400
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Truenorth
4
- VERSION = '0.2.6'
4
+ VERSION = '0.2.8'
5
5
  end
data/truenorth.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = 'truenorth'
5
- spec.version = '0.2.6'
5
+ spec.version = '0.2.8'
6
6
  spec.authors = ['usiegj00']
7
7
  spec.email = ['112138+usiegj00@users.noreply.github.com']
8
8
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: truenorth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - usiegj00