gsc-cli 2.2.3 → 2.2.5

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: c6cfd0972501711b114e27b2a286612efcfe85bd51aaf91947bc44b8fdfd9aff
4
- data.tar.gz: 5bd6e630844f13a3115bbb4c54deda04f7f3e1585c79ac4d9037dd7c216d02a1
3
+ metadata.gz: b146c585e5fdbbea6d4b61644f052df6e75b418d64666d4b4b6006825f0e6eaf
4
+ data.tar.gz: 04b24385606b02d483f14db0759848890aba51714b2fca731b483d9d88bf3346
5
5
  SHA512:
6
- metadata.gz: ab41e83aa4f41ca0d296218f288cb2291275c893cf60347d6cbc7083371430e06feebf00aa084d5c5978fd0bd0bd5a35f29a1beaf5839f322f2efc629bf38429
7
- data.tar.gz: 50ea5330dee9bae3b09570be862e69c3b4ec5e1aafc7126525bdd2dbe1a894c2392204749b5c7f187d32c0273530cc55f6ba66599502068b8870f1a7bdcd3f6b
6
+ metadata.gz: 3e2fa01bad3d160440d6ec93be6738dfeba193e7a89e380132b1f0f4c6abd8d2f311746ebe850c5af0cd95f303e3474996d0e640bdf336e2deec50482bedac4b
7
+ data.tar.gz: 9bbab9f7af4f23c285c7c22e5b8012a71633832a3c899b00ccf5e3f1ac1610b11bde8702cd6b5c6868641f0d55b359fdf72dba16d55cd877f662f5ba5134464a
data/README.md CHANGED
@@ -494,13 +494,19 @@ gsc use 2
494
494
  | `gsc index <url>` | — | Priority Googlebot crawl submission (`URL_UPDATED`) | `--dry-run`, `--json` |
495
495
  | `gsc remove <url>` | — | Notify Googlebot of permanently deleted URL (`URL_DELETED`) | `--dry-run`, `--json` |
496
496
  | `gsc status <url>` | — | Check Google Indexing API submission status and latest notification timestamp| `--json` |
497
- | `gsc index-batch` | — | Batch URL indexing processor with daily 200-URL quota tracking | `--run`, `--status`, `--json` |
497
+ | `gsc index-batch` | — | Batch URL indexing processor with daily 200-URL quota tracking | `--batch-size`, `--force`, `--dry-run`, `--json` |
498
498
  | `gsc indexnow <url>` | — | Multi-engine instant submission (Bing, Yandex, Seznam, Naver) | `--key`, `--json` |
499
499
  | `gsc zombies <sitemap>` | — | Detect zero-impression deadweight URLs wasting crawl budget over 90 days | `--days`, `--json` |
500
500
  | `gsc sitemaps-list` | — | List registered XML sitemaps in Search Console | `--json` |
501
501
  | `gsc sitemaps-submit <url>`| — | Submit or re-submit an XML sitemap to Search Console | `--json` |
502
502
  | `gsc sitemap-tree <url>` | — | Visual sitemap hierarchy tree and URL limit validator | `--json` |
503
503
 
504
+ > 🛡️ **Batch Indexing Safety Guard (`gsc index-batch clear`)**:
505
+ > When automating bulk indexing in scripts, CI/CD, or AI agents, an accidental `clear` could wipe hundreds of queued URLs waiting to be indexed. To prevent queue loss:
506
+ > - **Interactive Shells**: Requires explicit `[y/N]` user confirmation.
507
+ > - **Non-Interactive / Scripts / Agents**: Requires `--force` / `-f` (aborts with exit code `1` if omitted).
508
+ > - **Automatic Backup**: Every clear automatically snapshots the pending queue to `~/.gsc/backups/queue_backup_<TIMESTAMP>.json` before wiping, so URLs are never lost.
509
+
504
510
  ### 6. Keyword Research & Demand Trends
505
511
  | Command | Shortcut | Description | Flags |
506
512
  |---|---|---|---|
data/bin/gsc CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # ==============================================================================
5
5
  # Google Search Console & Indexing API CLI Tool (Pure Ruby - Zero Gem Dependencies)
6
- # Standalone Single-File Distribution (Built from lib/gsc v2.2.3)
6
+ # Standalone Single-File Distribution (Built from lib/gsc v2.2.5)
7
7
  # ==============================================================================
8
8
 
9
9
  require 'net/http'
@@ -20,7 +20,7 @@ require 'stringio'
20
20
 
21
21
  # --- version.rb ---
22
22
  module GSC
23
- VERSION = '2.2.3'
23
+ VERSION = '2.2.5'
24
24
  end
25
25
 
26
26
  # --- color.rb ---
@@ -70,7 +70,13 @@ module GSC
70
70
  CONFIG_DIR = File.expand_path('~/.config/gsc')
71
71
  CONFIG_FILE = File.join(CONFIG_DIR, 'config.json')
72
72
 
73
- def self.get(key)
73
+ def self.ensure_gsc_symlink!
74
+ gsc_dir = File.expand_path('~/.gsc')
75
+ return if File.exist?(gsc_dir) || File.symlink?(gsc_dir)
76
+ File.symlink(CONFIG_DIR, gsc_dir) rescue nil
77
+ end
78
+
79
+ def self.get(key)
74
80
  load[key.to_s]
75
81
  end
76
82
 
@@ -3174,6 +3180,29 @@ module GSC
3174
3180
 
3175
3181
  overall_category = data.dig('loadingExperience', 'overall_category') || data.dig('originLoadingExperience', 'overall_category')
3176
3182
 
3183
+ # Official Google Core Web Vitals Assessment evaluates ONLY the 3 Core Web Vitals:
3184
+ # LCP, INP (or legacy FID), and CLS. (FCP and TTFB are diagnostic metrics).
3185
+ # - PASSED: All present CWV metrics are 'FAST' (Good).
3186
+ # - POOR: Any CWV metric is 'SLOW' (Poor).
3187
+ # - NEEDS IMPROVEMENT: No CWV metric is 'SLOW', but at least one is 'AVERAGE'.
3188
+ cwv_keys = ['LARGEST_CONTENTFUL_PAINT_MS', 'INTERACTION_TO_NEXT_PAINT', 'CUMULATIVE_LAYOUT_SHIFT_SCORE']
3189
+ cwv_keys[1] = 'FIRST_INPUT_DELAY_MS' if !crux_metrics.key?('INTERACTION_TO_NEXT_PAINT') && crux_metrics.key?('FIRST_INPUT_DELAY_MS')
3190
+
3191
+ active_cwv = cwv_keys.map { |k| crux_metrics[k] }.compact
3192
+ cwv_assessment = if active_cwv.any?
3193
+ if active_cwv.any? { |m| m[:category] == 'SLOW' }
3194
+ 'POOR'
3195
+ elsif active_cwv.any? { |m| m[:category] == 'AVERAGE' }
3196
+ 'NEEDS IMPROVEMENT'
3197
+ elsif active_cwv.all? { |m| m[:category] == 'FAST' }
3198
+ 'PASSED'
3199
+ else
3200
+ overall_category
3201
+ end
3202
+ else
3203
+ overall_category
3204
+ end
3205
+
3177
3206
  # Opportunities
3178
3207
  opportunities = []
3179
3208
  audits.each do |k, v|
@@ -3205,6 +3234,7 @@ module GSC
3205
3234
  field_data: crux_metrics,
3206
3235
  field_source: crux_source,
3207
3236
  overall_category: overall_category,
3237
+ cwv_assessment: cwv_assessment,
3208
3238
  opportunities: opportunities.first(5)
3209
3239
  }
3210
3240
  end
@@ -5405,13 +5435,16 @@ require 'time'
5405
5435
  module GSC
5406
5436
  class IndexingQueue
5407
5437
  QUEUE_FILE = File.join(Config::CONFIG_DIR, 'indexing_queue.json')
5438
+ BACKUPS_DIR = File.join(Config::CONFIG_DIR, 'backups')
5408
5439
  DAILY_LIMIT = 200
5409
5440
 
5410
- attr_reader :state
5441
+ attr_reader :state, :file_path, :backup_dir
5411
5442
 
5412
- def initialize(file_path = QUEUE_FILE)
5443
+ def initialize(file_path = QUEUE_FILE, backup_dir: nil)
5413
5444
  @file_path = file_path
5445
+ @backup_dir = backup_dir || File.join(File.dirname(@file_path), 'backups')
5414
5446
  @state = load_state
5447
+ Config.ensure_gsc_symlink!
5415
5448
  end
5416
5449
 
5417
5450
  def add_urls(urls)
@@ -5443,7 +5476,41 @@ module GSC
5443
5476
  }
5444
5477
  end
5445
5478
 
5446
- def clear(scope = :all)
5479
+ def backup(scope = :all)
5480
+ pending = @state['pending'] || []
5481
+ submitted = @state['submitted'] || []
5482
+ failed = @state['failed'] || []
5483
+
5484
+ # Nothing to backup if completely empty
5485
+ return nil if pending.empty? && submitted.empty? && failed.empty?
5486
+
5487
+ FileUtils.mkdir_p(@backup_dir)
5488
+ timestamp = Time.now.utc.strftime('%Y%m%d_%H%M%S')
5489
+ backup_file = File.join(@backup_dir, "queue_backup_#{timestamp}.json")
5490
+ if File.exist?(backup_file)
5491
+ backup_file = File.join(@backup_dir, "queue_backup_#{timestamp}_#{Time.now.to_f.to_s.split('.').last}.json")
5492
+ end
5493
+
5494
+ snapshot = {
5495
+ 'timestamp' => Time.now.utc.iso8601,
5496
+ 'cleared_scope' => scope.to_s,
5497
+ 'count' => pending.size,
5498
+ 'pending' => pending,
5499
+ 'submitted' => submitted,
5500
+ 'failed' => failed,
5501
+ 'daily_quota_used' => @state['daily_quota_used'] || 0,
5502
+ 'last_reset_date' => @state['last_reset_date']
5503
+ }
5504
+
5505
+ File.write(backup_file, JSON.pretty_generate(snapshot))
5506
+ backup_file
5507
+ end
5508
+
5509
+ def clear(scope = :all, backup: true)
5510
+ pending = @state['pending'] || []
5511
+ count = pending.size
5512
+ backup_file = backup ? backup(scope) : nil
5513
+
5447
5514
  if scope == :pending
5448
5515
  @state['pending'] = []
5449
5516
  else
@@ -5452,6 +5519,8 @@ module GSC
5452
5519
  @state['failed'] = []
5453
5520
  end
5454
5521
  save_state
5522
+
5523
+ { count: count, backup_file: backup_file }
5455
5524
  end
5456
5525
 
5457
5526
  def process_batch(api, batch_size: 50, dry_run: false, delay_sec: 0.15)
@@ -9039,7 +9108,7 @@ COMMAND_REGISTRY = [
9039
9108
  commands: [
9040
9109
  { name: "inspect <url>", shortcut: nil, desc: "Live Google index check (coverage, canonical, date, robots.txt)", flags: ["-d"] },
9041
9110
  { name: "index <url>", shortcut: nil, desc: "Notify Googlebot to crawl/index URL immediately (URL_UPDATED)", flags: ["--dry-run"] },
9042
- { name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--json"] },
9111
+ { name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--force", "--json"] },
9043
9112
  { name: "remove <url>", shortcut: nil, desc: "Notify Googlebot a page has been deleted (URL_DELETED)", flags: ["--dry-run"] },
9044
9113
  { name: "status <url>", shortcut: nil, desc: "Check Google Indexing API notification metadata", flags: [] },
9045
9114
  { name: "sitemaps-list", shortcut: nil, desc: "List registered XML sitemaps in Search Console", flags: ["-d"] },
@@ -21010,11 +21079,54 @@ module GSC
21010
21079
  end
21011
21080
 
21012
21081
  when 'clear', 'reset'
21013
- queue.clear(:all)
21082
+ count = (queue.state['pending'] || []).size
21083
+ if count == 0
21084
+ if options[:json]
21085
+ puts JSON.pretty_generate({ ok: true, message: 'Batch queue is already empty.', count: 0 })
21086
+ else
21087
+ puts "Batch queue is already empty."
21088
+ end
21089
+ return
21090
+ end
21091
+
21092
+ unless options[:force]
21093
+ if $stdin.tty?
21094
+ $stdout.print "Are you sure you want to clear #{count} pending URLs from the batch queue? [y/N]: "
21095
+ $stdout.flush
21096
+ ans = $stdin.gets.to_s.strip.downcase
21097
+ unless ans == 'y' || ans == 'yes'
21098
+ if options[:json]
21099
+ puts JSON.pretty_generate({ ok: false, message: 'Clear aborted. Queue preserved.' })
21100
+ else
21101
+ puts "Clear aborted. Queue preserved."
21102
+ end
21103
+ return
21104
+ end
21105
+ else
21106
+ error_msg = "Error: 'gsc index-batch clear' requires '--force' in non-interactive environments to prevent accidental queue loss."
21107
+ if options[:json]
21108
+ puts JSON.pretty_generate({ error: error_msg })
21109
+ else
21110
+ puts error_msg
21111
+ end
21112
+ exit 1
21113
+ end
21114
+ end
21115
+
21116
+ res = queue.clear(:all, backup: true)
21117
+ backup_path = res[:backup_file]
21118
+ display_backup = backup_path ? backup_path.sub(/^#{Regexp.escape(Dir.home)}\/\.config\/gsc/, '~/.gsc').sub(/^#{Regexp.escape(Dir.home)}/, '~') : 'none'
21119
+ cleared_count = res[:count] || count
21120
+
21014
21121
  if options[:json]
21015
- puts JSON.pretty_generate({ ok: true, message: 'Queue cleared' })
21122
+ puts JSON.pretty_generate({
21123
+ ok: true,
21124
+ message: "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}",
21125
+ cleared_count: cleared_count,
21126
+ backup_file: backup_path
21127
+ })
21016
21128
  else
21017
- puts Color.c("🗑️ Indexing queue cleared.", Color::GREEN)
21129
+ puts "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}"
21018
21130
  end
21019
21131
 
21020
21132
  else
@@ -21032,7 +21144,13 @@ module GSC
21032
21144
  puts "\nCommands:"
21033
21145
  puts " • #{Color.c('gsc index-batch add <url|sitemap.xml|file.txt>', Color::CYAN)} - Enqueue URLs"
21034
21146
  puts " • #{Color.c('gsc index-batch run [--batch-size 50] [--dry-run]', Color::CYAN)} - Process queue"
21035
- puts " • #{Color.c('gsc index-batch clear', Color::CYAN)} - Reset queue"
21147
+ puts " • #{Color.c('gsc index-batch clear [--force]', Color::CYAN)} - Reset queue (safe wipe)"
21148
+ puts ""
21149
+ puts "🛡️ #{Color::BOLD}Queue Safety Guard:#{Color::RESET}"
21150
+ puts " • Prevents accidental queue loss from scripts, cron, or AI agents"
21151
+ puts " • Interactive: Prompts for confirmation [y/N] before clearing"
21152
+ puts " • Non-interactive / CI: Requires #{Color.c('--force', Color::YELLOW)} flag to proceed"
21153
+ puts " • Snapshot backup: Automatically saves a backup to ~/.gsc/backups/ before clearing"
21036
21154
  puts ""
21037
21155
  end
21038
21156
  end
@@ -22857,10 +22975,19 @@ module GSC
22857
22975
  fd = data[:field_data] || {}
22858
22976
  if fd.any?
22859
22977
  source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
22860
- overall = data[:overall_category] || "UNKNOWN"
22861
- overall_col = overall == 'FAST' ? Color::GREEN : (overall == 'AVERAGE' ? Color::YELLOW : Color::RED)
22978
+ assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
22979
+ assessment_badge = case assessment
22980
+ when 'PASSED', 'FAST'
22981
+ Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
22982
+ when 'NEEDS IMPROVEMENT', 'AVERAGE'
22983
+ Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
22984
+ when 'POOR', 'SLOW', 'FAILED'
22985
+ Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
22986
+ else
22987
+ Color.c(assessment, Color::YELLOW, Color::BOLD)
22988
+ end
22862
22989
  puts "\n#{Color::BOLD}🌐 CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
22863
- puts " • Core Web Vitals Status : #{Color.c(overall, overall_col, Color::BOLD)}"
22990
+ puts " • Core Web Vitals Assessment : #{assessment_badge}"
22864
22991
 
22865
22992
  if fd['LARGEST_CONTENTFUL_PAINT_MS']
22866
22993
  val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
@@ -30016,10 +30143,19 @@ module GSC
30016
30143
  fd = data[:field_data] || {}
30017
30144
  if fd.any?
30018
30145
  source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
30019
- overall = data[:overall_category] || "UNKNOWN"
30020
- overall_col = overall == 'FAST' ? Color::GREEN : (overall == 'AVERAGE' ? Color::YELLOW : Color::RED)
30146
+ assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
30147
+ assessment_badge = case assessment
30148
+ when 'PASSED', 'FAST'
30149
+ Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
30150
+ when 'NEEDS IMPROVEMENT', 'AVERAGE'
30151
+ Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
30152
+ when 'POOR', 'SLOW', 'FAILED'
30153
+ Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
30154
+ else
30155
+ Color.c(assessment, Color::YELLOW, Color::BOLD)
30156
+ end
30021
30157
  puts "\n#{Color::BOLD}🌐 CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
30022
- puts " • Core Web Vitals Status : #{Color.c(overall, overall_col, Color::BOLD)}"
30158
+ puts " • Core Web Vitals Assessment : #{assessment_badge}"
30023
30159
 
30024
30160
  if fd['LARGEST_CONTENTFUL_PAINT_MS']
30025
30161
  val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
@@ -31529,6 +31665,10 @@ module GSC
31529
31665
  options[:batch_size] = size
31530
31666
  end
31531
31667
 
31668
+ opts.on('-f', '--force', '--yes', '-y', 'Bypass confirmation prompts (required for non-interactive queue clears)') do
31669
+ options[:force] = true
31670
+ end
31671
+
31532
31672
  opts.on('--concurrency COUNT', Integer, 'Concurrent threads for multi-page site crawler (default: 5, max: 20)') do |c|
31533
31673
  options[:concurrency] = c
31534
31674
  end
@@ -32185,6 +32325,14 @@ module GSC
32185
32325
  Audit.run(command, target, extra, options)
32186
32326
  exit 0 unless options[:in_dashboard]
32187
32327
  return
32328
+
32329
+ when 'index-batch', 'queue', 'batch-index'
32330
+ action = target.to_s.strip.downcase
32331
+ if action != 'run' && action != 'process' && action != 'execute'
32332
+ Indexing.run(command, target, extra, options, nil, nil, nil, nil)
32333
+ exit 0 unless options[:in_dashboard]
32334
+ return
32335
+ end
32188
32336
  end
32189
32337
 
32190
32338
  # 7. Authenticated Commands (GSC / Indexing / Analytics / GA4)
data/dist/gsc CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # ==============================================================================
5
5
  # Google Search Console & Indexing API CLI Tool (Pure Ruby - Zero Gem Dependencies)
6
- # Standalone Single-File Distribution (Built from lib/gsc v2.2.3)
6
+ # Standalone Single-File Distribution (Built from lib/gsc v2.2.5)
7
7
  # ==============================================================================
8
8
 
9
9
  require 'net/http'
@@ -20,7 +20,7 @@ require 'stringio'
20
20
 
21
21
  # --- version.rb ---
22
22
  module GSC
23
- VERSION = '2.2.3'
23
+ VERSION = '2.2.5'
24
24
  end
25
25
 
26
26
  # --- color.rb ---
@@ -70,7 +70,13 @@ module GSC
70
70
  CONFIG_DIR = File.expand_path('~/.config/gsc')
71
71
  CONFIG_FILE = File.join(CONFIG_DIR, 'config.json')
72
72
 
73
- def self.get(key)
73
+ def self.ensure_gsc_symlink!
74
+ gsc_dir = File.expand_path('~/.gsc')
75
+ return if File.exist?(gsc_dir) || File.symlink?(gsc_dir)
76
+ File.symlink(CONFIG_DIR, gsc_dir) rescue nil
77
+ end
78
+
79
+ def self.get(key)
74
80
  load[key.to_s]
75
81
  end
76
82
 
@@ -3174,6 +3180,29 @@ module GSC
3174
3180
 
3175
3181
  overall_category = data.dig('loadingExperience', 'overall_category') || data.dig('originLoadingExperience', 'overall_category')
3176
3182
 
3183
+ # Official Google Core Web Vitals Assessment evaluates ONLY the 3 Core Web Vitals:
3184
+ # LCP, INP (or legacy FID), and CLS. (FCP and TTFB are diagnostic metrics).
3185
+ # - PASSED: All present CWV metrics are 'FAST' (Good).
3186
+ # - POOR: Any CWV metric is 'SLOW' (Poor).
3187
+ # - NEEDS IMPROVEMENT: No CWV metric is 'SLOW', but at least one is 'AVERAGE'.
3188
+ cwv_keys = ['LARGEST_CONTENTFUL_PAINT_MS', 'INTERACTION_TO_NEXT_PAINT', 'CUMULATIVE_LAYOUT_SHIFT_SCORE']
3189
+ cwv_keys[1] = 'FIRST_INPUT_DELAY_MS' if !crux_metrics.key?('INTERACTION_TO_NEXT_PAINT') && crux_metrics.key?('FIRST_INPUT_DELAY_MS')
3190
+
3191
+ active_cwv = cwv_keys.map { |k| crux_metrics[k] }.compact
3192
+ cwv_assessment = if active_cwv.any?
3193
+ if active_cwv.any? { |m| m[:category] == 'SLOW' }
3194
+ 'POOR'
3195
+ elsif active_cwv.any? { |m| m[:category] == 'AVERAGE' }
3196
+ 'NEEDS IMPROVEMENT'
3197
+ elsif active_cwv.all? { |m| m[:category] == 'FAST' }
3198
+ 'PASSED'
3199
+ else
3200
+ overall_category
3201
+ end
3202
+ else
3203
+ overall_category
3204
+ end
3205
+
3177
3206
  # Opportunities
3178
3207
  opportunities = []
3179
3208
  audits.each do |k, v|
@@ -3205,6 +3234,7 @@ module GSC
3205
3234
  field_data: crux_metrics,
3206
3235
  field_source: crux_source,
3207
3236
  overall_category: overall_category,
3237
+ cwv_assessment: cwv_assessment,
3208
3238
  opportunities: opportunities.first(5)
3209
3239
  }
3210
3240
  end
@@ -5405,13 +5435,16 @@ require 'time'
5405
5435
  module GSC
5406
5436
  class IndexingQueue
5407
5437
  QUEUE_FILE = File.join(Config::CONFIG_DIR, 'indexing_queue.json')
5438
+ BACKUPS_DIR = File.join(Config::CONFIG_DIR, 'backups')
5408
5439
  DAILY_LIMIT = 200
5409
5440
 
5410
- attr_reader :state
5441
+ attr_reader :state, :file_path, :backup_dir
5411
5442
 
5412
- def initialize(file_path = QUEUE_FILE)
5443
+ def initialize(file_path = QUEUE_FILE, backup_dir: nil)
5413
5444
  @file_path = file_path
5445
+ @backup_dir = backup_dir || File.join(File.dirname(@file_path), 'backups')
5414
5446
  @state = load_state
5447
+ Config.ensure_gsc_symlink!
5415
5448
  end
5416
5449
 
5417
5450
  def add_urls(urls)
@@ -5443,7 +5476,41 @@ module GSC
5443
5476
  }
5444
5477
  end
5445
5478
 
5446
- def clear(scope = :all)
5479
+ def backup(scope = :all)
5480
+ pending = @state['pending'] || []
5481
+ submitted = @state['submitted'] || []
5482
+ failed = @state['failed'] || []
5483
+
5484
+ # Nothing to backup if completely empty
5485
+ return nil if pending.empty? && submitted.empty? && failed.empty?
5486
+
5487
+ FileUtils.mkdir_p(@backup_dir)
5488
+ timestamp = Time.now.utc.strftime('%Y%m%d_%H%M%S')
5489
+ backup_file = File.join(@backup_dir, "queue_backup_#{timestamp}.json")
5490
+ if File.exist?(backup_file)
5491
+ backup_file = File.join(@backup_dir, "queue_backup_#{timestamp}_#{Time.now.to_f.to_s.split('.').last}.json")
5492
+ end
5493
+
5494
+ snapshot = {
5495
+ 'timestamp' => Time.now.utc.iso8601,
5496
+ 'cleared_scope' => scope.to_s,
5497
+ 'count' => pending.size,
5498
+ 'pending' => pending,
5499
+ 'submitted' => submitted,
5500
+ 'failed' => failed,
5501
+ 'daily_quota_used' => @state['daily_quota_used'] || 0,
5502
+ 'last_reset_date' => @state['last_reset_date']
5503
+ }
5504
+
5505
+ File.write(backup_file, JSON.pretty_generate(snapshot))
5506
+ backup_file
5507
+ end
5508
+
5509
+ def clear(scope = :all, backup: true)
5510
+ pending = @state['pending'] || []
5511
+ count = pending.size
5512
+ backup_file = backup ? backup(scope) : nil
5513
+
5447
5514
  if scope == :pending
5448
5515
  @state['pending'] = []
5449
5516
  else
@@ -5452,6 +5519,8 @@ module GSC
5452
5519
  @state['failed'] = []
5453
5520
  end
5454
5521
  save_state
5522
+
5523
+ { count: count, backup_file: backup_file }
5455
5524
  end
5456
5525
 
5457
5526
  def process_batch(api, batch_size: 50, dry_run: false, delay_sec: 0.15)
@@ -9039,7 +9108,7 @@ COMMAND_REGISTRY = [
9039
9108
  commands: [
9040
9109
  { name: "inspect <url>", shortcut: nil, desc: "Live Google index check (coverage, canonical, date, robots.txt)", flags: ["-d"] },
9041
9110
  { name: "index <url>", shortcut: nil, desc: "Notify Googlebot to crawl/index URL immediately (URL_UPDATED)", flags: ["--dry-run"] },
9042
- { name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--json"] },
9111
+ { name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--force", "--json"] },
9043
9112
  { name: "remove <url>", shortcut: nil, desc: "Notify Googlebot a page has been deleted (URL_DELETED)", flags: ["--dry-run"] },
9044
9113
  { name: "status <url>", shortcut: nil, desc: "Check Google Indexing API notification metadata", flags: [] },
9045
9114
  { name: "sitemaps-list", shortcut: nil, desc: "List registered XML sitemaps in Search Console", flags: ["-d"] },
@@ -21010,11 +21079,54 @@ module GSC
21010
21079
  end
21011
21080
 
21012
21081
  when 'clear', 'reset'
21013
- queue.clear(:all)
21082
+ count = (queue.state['pending'] || []).size
21083
+ if count == 0
21084
+ if options[:json]
21085
+ puts JSON.pretty_generate({ ok: true, message: 'Batch queue is already empty.', count: 0 })
21086
+ else
21087
+ puts "Batch queue is already empty."
21088
+ end
21089
+ return
21090
+ end
21091
+
21092
+ unless options[:force]
21093
+ if $stdin.tty?
21094
+ $stdout.print "Are you sure you want to clear #{count} pending URLs from the batch queue? [y/N]: "
21095
+ $stdout.flush
21096
+ ans = $stdin.gets.to_s.strip.downcase
21097
+ unless ans == 'y' || ans == 'yes'
21098
+ if options[:json]
21099
+ puts JSON.pretty_generate({ ok: false, message: 'Clear aborted. Queue preserved.' })
21100
+ else
21101
+ puts "Clear aborted. Queue preserved."
21102
+ end
21103
+ return
21104
+ end
21105
+ else
21106
+ error_msg = "Error: 'gsc index-batch clear' requires '--force' in non-interactive environments to prevent accidental queue loss."
21107
+ if options[:json]
21108
+ puts JSON.pretty_generate({ error: error_msg })
21109
+ else
21110
+ puts error_msg
21111
+ end
21112
+ exit 1
21113
+ end
21114
+ end
21115
+
21116
+ res = queue.clear(:all, backup: true)
21117
+ backup_path = res[:backup_file]
21118
+ display_backup = backup_path ? backup_path.sub(/^#{Regexp.escape(Dir.home)}\/\.config\/gsc/, '~/.gsc').sub(/^#{Regexp.escape(Dir.home)}/, '~') : 'none'
21119
+ cleared_count = res[:count] || count
21120
+
21014
21121
  if options[:json]
21015
- puts JSON.pretty_generate({ ok: true, message: 'Queue cleared' })
21122
+ puts JSON.pretty_generate({
21123
+ ok: true,
21124
+ message: "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}",
21125
+ cleared_count: cleared_count,
21126
+ backup_file: backup_path
21127
+ })
21016
21128
  else
21017
- puts Color.c("🗑️ Indexing queue cleared.", Color::GREEN)
21129
+ puts "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}"
21018
21130
  end
21019
21131
 
21020
21132
  else
@@ -21032,7 +21144,13 @@ module GSC
21032
21144
  puts "\nCommands:"
21033
21145
  puts " • #{Color.c('gsc index-batch add <url|sitemap.xml|file.txt>', Color::CYAN)} - Enqueue URLs"
21034
21146
  puts " • #{Color.c('gsc index-batch run [--batch-size 50] [--dry-run]', Color::CYAN)} - Process queue"
21035
- puts " • #{Color.c('gsc index-batch clear', Color::CYAN)} - Reset queue"
21147
+ puts " • #{Color.c('gsc index-batch clear [--force]', Color::CYAN)} - Reset queue (safe wipe)"
21148
+ puts ""
21149
+ puts "🛡️ #{Color::BOLD}Queue Safety Guard:#{Color::RESET}"
21150
+ puts " • Prevents accidental queue loss from scripts, cron, or AI agents"
21151
+ puts " • Interactive: Prompts for confirmation [y/N] before clearing"
21152
+ puts " • Non-interactive / CI: Requires #{Color.c('--force', Color::YELLOW)} flag to proceed"
21153
+ puts " • Snapshot backup: Automatically saves a backup to ~/.gsc/backups/ before clearing"
21036
21154
  puts ""
21037
21155
  end
21038
21156
  end
@@ -22857,10 +22975,19 @@ module GSC
22857
22975
  fd = data[:field_data] || {}
22858
22976
  if fd.any?
22859
22977
  source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
22860
- overall = data[:overall_category] || "UNKNOWN"
22861
- overall_col = overall == 'FAST' ? Color::GREEN : (overall == 'AVERAGE' ? Color::YELLOW : Color::RED)
22978
+ assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
22979
+ assessment_badge = case assessment
22980
+ when 'PASSED', 'FAST'
22981
+ Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
22982
+ when 'NEEDS IMPROVEMENT', 'AVERAGE'
22983
+ Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
22984
+ when 'POOR', 'SLOW', 'FAILED'
22985
+ Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
22986
+ else
22987
+ Color.c(assessment, Color::YELLOW, Color::BOLD)
22988
+ end
22862
22989
  puts "\n#{Color::BOLD}🌐 CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
22863
- puts " • Core Web Vitals Status : #{Color.c(overall, overall_col, Color::BOLD)}"
22990
+ puts " • Core Web Vitals Assessment : #{assessment_badge}"
22864
22991
 
22865
22992
  if fd['LARGEST_CONTENTFUL_PAINT_MS']
22866
22993
  val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
@@ -30016,10 +30143,19 @@ module GSC
30016
30143
  fd = data[:field_data] || {}
30017
30144
  if fd.any?
30018
30145
  source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
30019
- overall = data[:overall_category] || "UNKNOWN"
30020
- overall_col = overall == 'FAST' ? Color::GREEN : (overall == 'AVERAGE' ? Color::YELLOW : Color::RED)
30146
+ assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
30147
+ assessment_badge = case assessment
30148
+ when 'PASSED', 'FAST'
30149
+ Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
30150
+ when 'NEEDS IMPROVEMENT', 'AVERAGE'
30151
+ Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
30152
+ when 'POOR', 'SLOW', 'FAILED'
30153
+ Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
30154
+ else
30155
+ Color.c(assessment, Color::YELLOW, Color::BOLD)
30156
+ end
30021
30157
  puts "\n#{Color::BOLD}🌐 CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
30022
- puts " • Core Web Vitals Status : #{Color.c(overall, overall_col, Color::BOLD)}"
30158
+ puts " • Core Web Vitals Assessment : #{assessment_badge}"
30023
30159
 
30024
30160
  if fd['LARGEST_CONTENTFUL_PAINT_MS']
30025
30161
  val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
@@ -31529,6 +31665,10 @@ module GSC
31529
31665
  options[:batch_size] = size
31530
31666
  end
31531
31667
 
31668
+ opts.on('-f', '--force', '--yes', '-y', 'Bypass confirmation prompts (required for non-interactive queue clears)') do
31669
+ options[:force] = true
31670
+ end
31671
+
31532
31672
  opts.on('--concurrency COUNT', Integer, 'Concurrent threads for multi-page site crawler (default: 5, max: 20)') do |c|
31533
31673
  options[:concurrency] = c
31534
31674
  end
@@ -32185,6 +32325,14 @@ module GSC
32185
32325
  Audit.run(command, target, extra, options)
32186
32326
  exit 0 unless options[:in_dashboard]
32187
32327
  return
32328
+
32329
+ when 'index-batch', 'queue', 'batch-index'
32330
+ action = target.to_s.strip.downcase
32331
+ if action != 'run' && action != 'process' && action != 'execute'
32332
+ Indexing.run(command, target, extra, options, nil, nil, nil, nil)
32333
+ exit 0 unless options[:in_dashboard]
32334
+ return
32335
+ end
32188
32336
  end
32189
32337
 
32190
32338
  # 7. Authenticated Commands (GSC / Indexing / Analytics / GA4)
data/lib/gsc/cli/audit.rb CHANGED
@@ -170,10 +170,19 @@ module GSC
170
170
  fd = data[:field_data] || {}
171
171
  if fd.any?
172
172
  source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
173
- overall = data[:overall_category] || "UNKNOWN"
174
- overall_col = overall == 'FAST' ? Color::GREEN : (overall == 'AVERAGE' ? Color::YELLOW : Color::RED)
173
+ assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
174
+ assessment_badge = case assessment
175
+ when 'PASSED', 'FAST'
176
+ Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
177
+ when 'NEEDS IMPROVEMENT', 'AVERAGE'
178
+ Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
179
+ when 'POOR', 'SLOW', 'FAILED'
180
+ Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
181
+ else
182
+ Color.c(assessment, Color::YELLOW, Color::BOLD)
183
+ end
175
184
  puts "\n#{Color::BOLD}🌐 CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
176
- puts " • Core Web Vitals Status : #{Color.c(overall, overall_col, Color::BOLD)}"
185
+ puts " • Core Web Vitals Assessment : #{assessment_badge}"
177
186
 
178
187
  if fd['LARGEST_CONTENTFUL_PAINT_MS']
179
188
  val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
@@ -226,11 +226,54 @@ module GSC
226
226
  end
227
227
 
228
228
  when 'clear', 'reset'
229
- queue.clear(:all)
229
+ count = (queue.state['pending'] || []).size
230
+ if count == 0
231
+ if options[:json]
232
+ puts JSON.pretty_generate({ ok: true, message: 'Batch queue is already empty.', count: 0 })
233
+ else
234
+ puts "Batch queue is already empty."
235
+ end
236
+ return
237
+ end
238
+
239
+ unless options[:force]
240
+ if $stdin.tty?
241
+ $stdout.print "Are you sure you want to clear #{count} pending URLs from the batch queue? [y/N]: "
242
+ $stdout.flush
243
+ ans = $stdin.gets.to_s.strip.downcase
244
+ unless ans == 'y' || ans == 'yes'
245
+ if options[:json]
246
+ puts JSON.pretty_generate({ ok: false, message: 'Clear aborted. Queue preserved.' })
247
+ else
248
+ puts "Clear aborted. Queue preserved."
249
+ end
250
+ return
251
+ end
252
+ else
253
+ error_msg = "Error: 'gsc index-batch clear' requires '--force' in non-interactive environments to prevent accidental queue loss."
254
+ if options[:json]
255
+ puts JSON.pretty_generate({ error: error_msg })
256
+ else
257
+ puts error_msg
258
+ end
259
+ exit 1
260
+ end
261
+ end
262
+
263
+ res = queue.clear(:all, backup: true)
264
+ backup_path = res[:backup_file]
265
+ display_backup = backup_path ? backup_path.sub(/^#{Regexp.escape(Dir.home)}\/\.config\/gsc/, '~/.gsc').sub(/^#{Regexp.escape(Dir.home)}/, '~') : 'none'
266
+ cleared_count = res[:count] || count
267
+
230
268
  if options[:json]
231
- puts JSON.pretty_generate({ ok: true, message: 'Queue cleared' })
269
+ puts JSON.pretty_generate({
270
+ ok: true,
271
+ message: "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}",
272
+ cleared_count: cleared_count,
273
+ backup_file: backup_path
274
+ })
232
275
  else
233
- puts Color.c("🗑️ Indexing queue cleared.", Color::GREEN)
276
+ puts "Queue cleared (#{cleared_count} items). Backup saved to #{display_backup}"
234
277
  end
235
278
 
236
279
  else
@@ -248,7 +291,13 @@ module GSC
248
291
  puts "\nCommands:"
249
292
  puts " • #{Color.c('gsc index-batch add <url|sitemap.xml|file.txt>', Color::CYAN)} - Enqueue URLs"
250
293
  puts " • #{Color.c('gsc index-batch run [--batch-size 50] [--dry-run]', Color::CYAN)} - Process queue"
251
- puts " • #{Color.c('gsc index-batch clear', Color::CYAN)} - Reset queue"
294
+ puts " • #{Color.c('gsc index-batch clear [--force]', Color::CYAN)} - Reset queue (safe wipe)"
295
+ puts ""
296
+ puts "🛡️ #{Color::BOLD}Queue Safety Guard:#{Color::RESET}"
297
+ puts " • Prevents accidental queue loss from scripts, cron, or AI agents"
298
+ puts " • Interactive: Prompts for confirmation [y/N] before clearing"
299
+ puts " • Non-interactive / CI: Requires #{Color.c('--force', Color::YELLOW)} flag to proceed"
300
+ puts " • Snapshot backup: Automatically saves a backup to ~/.gsc/backups/ before clearing"
252
301
  puts ""
253
302
  end
254
303
  end
data/lib/gsc/cli.rb CHANGED
@@ -134,6 +134,10 @@ module GSC
134
134
  options[:batch_size] = size
135
135
  end
136
136
 
137
+ opts.on('-f', '--force', '--yes', '-y', 'Bypass confirmation prompts (required for non-interactive queue clears)') do
138
+ options[:force] = true
139
+ end
140
+
137
141
  opts.on('--concurrency COUNT', Integer, 'Concurrent threads for multi-page site crawler (default: 5, max: 20)') do |c|
138
142
  options[:concurrency] = c
139
143
  end
@@ -790,6 +794,14 @@ module GSC
790
794
  Audit.run(command, target, extra, options)
791
795
  exit 0 unless options[:in_dashboard]
792
796
  return
797
+
798
+ when 'index-batch', 'queue', 'batch-index'
799
+ action = target.to_s.strip.downcase
800
+ if action != 'run' && action != 'process' && action != 'execute'
801
+ Indexing.run(command, target, extra, options, nil, nil, nil, nil)
802
+ exit 0 unless options[:in_dashboard]
803
+ return
804
+ end
793
805
  end
794
806
 
795
807
  # 7. Authenticated Commands (GSC / Indexing / Analytics / GA4)
@@ -180,10 +180,19 @@ module GSC
180
180
  fd = data[:field_data] || {}
181
181
  if fd.any?
182
182
  source_label = data[:field_source] == :origin ? "Domain-Wide Origin RUM" : "URL-Level RUM"
183
- overall = data[:overall_category] || "UNKNOWN"
184
- overall_col = overall == 'FAST' ? Color::GREEN : (overall == 'AVERAGE' ? Color::YELLOW : Color::RED)
183
+ assessment = data[:cwv_assessment] || data[:overall_category] || "UNKNOWN"
184
+ assessment_badge = case assessment
185
+ when 'PASSED', 'FAST'
186
+ Color.c("PASSED (All 3 Metrics Good)", Color::GREEN, Color::BOLD)
187
+ when 'NEEDS IMPROVEMENT', 'AVERAGE'
188
+ Color.c("NEEDS IMPROVEMENT", Color::YELLOW, Color::BOLD)
189
+ when 'POOR', 'SLOW', 'FAILED'
190
+ Color.c("FAILED (Poor Metrics Detected)", Color::RED, Color::BOLD)
191
+ else
192
+ Color.c(assessment, Color::YELLOW, Color::BOLD)
193
+ end
185
194
  puts "\n#{Color::BOLD}🌐 CrUX FIELD DATA (28-Day Real User Monitoring - #{source_label}):#{Color::RESET}"
186
- puts " • Core Web Vitals Status : #{Color.c(overall, overall_col, Color::BOLD)}"
195
+ puts " • Core Web Vitals Assessment : #{assessment_badge}"
187
196
 
188
197
  if fd['LARGEST_CONTENTFUL_PAINT_MS']
189
198
  val = "#{(fd['LARGEST_CONTENTFUL_PAINT_MS'][:percentile].to_f / 1000).round(2)} s"
@@ -58,7 +58,7 @@ COMMAND_REGISTRY = [
58
58
  commands: [
59
59
  { name: "inspect <url>", shortcut: nil, desc: "Live Google index check (coverage, canonical, date, robots.txt)", flags: ["-d"] },
60
60
  { name: "index <url>", shortcut: nil, desc: "Notify Googlebot to crawl/index URL immediately (URL_UPDATED)", flags: ["--dry-run"] },
61
- { name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--json"] },
61
+ { name: "index-batch [action]", shortcut: "ib", desc: "Automated 200 URL/day quota queue manager (add, run, status, clear)", flags: ["--batch-size", "--dry-run", "--force", "--json"] },
62
62
  { name: "remove <url>", shortcut: nil, desc: "Notify Googlebot a page has been deleted (URL_DELETED)", flags: ["--dry-run"] },
63
63
  { name: "status <url>", shortcut: nil, desc: "Check Google Indexing API notification metadata", flags: [] },
64
64
  { name: "sitemaps-list", shortcut: nil, desc: "List registered XML sitemaps in Search Console", flags: ["-d"] },
data/lib/gsc/config.rb CHANGED
@@ -9,7 +9,13 @@ module GSC
9
9
  CONFIG_DIR = File.expand_path('~/.config/gsc')
10
10
  CONFIG_FILE = File.join(CONFIG_DIR, 'config.json')
11
11
 
12
- def self.get(key)
12
+ def self.ensure_gsc_symlink!
13
+ gsc_dir = File.expand_path('~/.gsc')
14
+ return if File.exist?(gsc_dir) || File.symlink?(gsc_dir)
15
+ File.symlink(CONFIG_DIR, gsc_dir) rescue nil
16
+ end
17
+
18
+ def self.get(key)
13
19
  load[key.to_s]
14
20
  end
15
21
 
@@ -8,13 +8,16 @@ require 'time'
8
8
  module GSC
9
9
  class IndexingQueue
10
10
  QUEUE_FILE = File.join(Config::CONFIG_DIR, 'indexing_queue.json')
11
+ BACKUPS_DIR = File.join(Config::CONFIG_DIR, 'backups')
11
12
  DAILY_LIMIT = 200
12
13
 
13
- attr_reader :state
14
+ attr_reader :state, :file_path, :backup_dir
14
15
 
15
- def initialize(file_path = QUEUE_FILE)
16
+ def initialize(file_path = QUEUE_FILE, backup_dir: nil)
16
17
  @file_path = file_path
18
+ @backup_dir = backup_dir || File.join(File.dirname(@file_path), 'backups')
17
19
  @state = load_state
20
+ Config.ensure_gsc_symlink!
18
21
  end
19
22
 
20
23
  def add_urls(urls)
@@ -46,7 +49,41 @@ module GSC
46
49
  }
47
50
  end
48
51
 
49
- def clear(scope = :all)
52
+ def backup(scope = :all)
53
+ pending = @state['pending'] || []
54
+ submitted = @state['submitted'] || []
55
+ failed = @state['failed'] || []
56
+
57
+ # Nothing to backup if completely empty
58
+ return nil if pending.empty? && submitted.empty? && failed.empty?
59
+
60
+ FileUtils.mkdir_p(@backup_dir)
61
+ timestamp = Time.now.utc.strftime('%Y%m%d_%H%M%S')
62
+ backup_file = File.join(@backup_dir, "queue_backup_#{timestamp}.json")
63
+ if File.exist?(backup_file)
64
+ backup_file = File.join(@backup_dir, "queue_backup_#{timestamp}_#{Time.now.to_f.to_s.split('.').last}.json")
65
+ end
66
+
67
+ snapshot = {
68
+ 'timestamp' => Time.now.utc.iso8601,
69
+ 'cleared_scope' => scope.to_s,
70
+ 'count' => pending.size,
71
+ 'pending' => pending,
72
+ 'submitted' => submitted,
73
+ 'failed' => failed,
74
+ 'daily_quota_used' => @state['daily_quota_used'] || 0,
75
+ 'last_reset_date' => @state['last_reset_date']
76
+ }
77
+
78
+ File.write(backup_file, JSON.pretty_generate(snapshot))
79
+ backup_file
80
+ end
81
+
82
+ def clear(scope = :all, backup: true)
83
+ pending = @state['pending'] || []
84
+ count = pending.size
85
+ backup_file = backup ? backup(scope) : nil
86
+
50
87
  if scope == :pending
51
88
  @state['pending'] = []
52
89
  else
@@ -55,6 +92,8 @@ module GSC
55
92
  @state['failed'] = []
56
93
  end
57
94
  save_state
95
+
96
+ { count: count, backup_file: backup_file }
58
97
  end
59
98
 
60
99
  def process_batch(api, batch_size: 50, dry_run: false, delay_sec: 0.15)
@@ -86,6 +86,29 @@ module GSC
86
86
 
87
87
  overall_category = data.dig('loadingExperience', 'overall_category') || data.dig('originLoadingExperience', 'overall_category')
88
88
 
89
+ # Official Google Core Web Vitals Assessment evaluates ONLY the 3 Core Web Vitals:
90
+ # LCP, INP (or legacy FID), and CLS. (FCP and TTFB are diagnostic metrics).
91
+ # - PASSED: All present CWV metrics are 'FAST' (Good).
92
+ # - POOR: Any CWV metric is 'SLOW' (Poor).
93
+ # - NEEDS IMPROVEMENT: No CWV metric is 'SLOW', but at least one is 'AVERAGE'.
94
+ cwv_keys = ['LARGEST_CONTENTFUL_PAINT_MS', 'INTERACTION_TO_NEXT_PAINT', 'CUMULATIVE_LAYOUT_SHIFT_SCORE']
95
+ cwv_keys[1] = 'FIRST_INPUT_DELAY_MS' if !crux_metrics.key?('INTERACTION_TO_NEXT_PAINT') && crux_metrics.key?('FIRST_INPUT_DELAY_MS')
96
+
97
+ active_cwv = cwv_keys.map { |k| crux_metrics[k] }.compact
98
+ cwv_assessment = if active_cwv.any?
99
+ if active_cwv.any? { |m| m[:category] == 'SLOW' }
100
+ 'POOR'
101
+ elsif active_cwv.any? { |m| m[:category] == 'AVERAGE' }
102
+ 'NEEDS IMPROVEMENT'
103
+ elsif active_cwv.all? { |m| m[:category] == 'FAST' }
104
+ 'PASSED'
105
+ else
106
+ overall_category
107
+ end
108
+ else
109
+ overall_category
110
+ end
111
+
89
112
  # Opportunities
90
113
  opportunities = []
91
114
  audits.each do |k, v|
@@ -117,6 +140,7 @@ module GSC
117
140
  field_data: crux_metrics,
118
141
  field_source: crux_source,
119
142
  overall_category: overall_category,
143
+ cwv_assessment: cwv_assessment,
120
144
  opportunities: opportunities.first(5)
121
145
  }
122
146
  end
data/lib/gsc/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GSC
4
- VERSION = '2.2.3'
4
+ VERSION = '2.2.5'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gsc-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.3
4
+ version: 2.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - ApollosWave LLC